從源碼解讀Vue生命周期 2021-03-21

基礎(chǔ)知識(shí)

Vue的生命周期

大自然有春夏秋冬,人有生老病死,優(yōu)秀的Vue當(dāng)然也存在自己的生命周期。

對(duì)于Vue來說它的生命周期就是Vue實(shí)例從創(chuàng)建到銷毀的過程。

生命周期函數(shù)

在生命周期的過程中運(yùn)行著一些叫做生命周期的函數(shù),給予了開發(fā)者在不同的生命周期階段添加業(yè)務(wù)代碼的能力。

在網(wǎng)上的一些文章中有的也叫它們生命周期鉤子,那鉤子又是什么呢?

鉤子函數(shù)

其實(shí)和回調(diào)是一個(gè)概念,當(dāng)系統(tǒng)執(zhí)行到某處時(shí),檢查是否有hook(鉤子),有的話就會(huì)執(zhí)行回調(diào)。

此hook非彼hook。

通俗的說,hook就是在程序運(yùn)行中,在某個(gè)特定的位置,框架的開發(fā)者設(shè)計(jì)好了一個(gè)鉤子來告訴我們當(dāng)前程序已經(jīng)運(yùn)行到特定的位置了,會(huì)觸發(fā)一個(gè)回調(diào)函數(shù),并提供給我們,讓我們可以在生命周期的特定階段進(jìn)行相關(guān)業(yè)務(wù)代碼的編寫。

我在官方提供的圖片上添加了相關(guān)注釋,希望能夠讓大家看的更明白一些,如下圖。

圖片描述

雖然添加了很多注釋,看不懂不要慌,我們來逐一進(jìn)行講解。

總的來說,Vue的生命周期可以分為以下八個(gè)階段:

beforeCreate 實(shí)例創(chuàng)建前

created 實(shí)例創(chuàng)建完成

beforeMount 掛載前

mounted 掛載完成

beforeUpdate 更新前

updated 更新完成

beforeDestory 銷毀前

destoryed 銷毀完成

1.beforeCreate

這個(gè)鉤子是new Vue()之后觸發(fā)的第一個(gè)鉤子,在當(dāng)前階段中data、methods、computed以及watch上的數(shù)據(jù)和方法均不能被訪問。

2.created

這個(gè)鉤子在實(shí)例創(chuàng)建完成后發(fā)生,當(dāng)前階段已經(jīng)完成了數(shù)據(jù)觀測(cè),也就是可以使用數(shù)據(jù),更改數(shù)據(jù),在這里更改數(shù)據(jù)不會(huì)觸發(fā)updated函數(shù)。可以做一些初始數(shù)據(jù)的獲取,在當(dāng)前階段無法與Dom進(jìn)行交互,如果你非要想,可以通過vm.$nextTick來訪問Dom。

3.beforeMounted

這個(gè)鉤子發(fā)生在掛載之前,在這之前template模板已導(dǎo)入渲染函數(shù)編譯。而當(dāng)前階段虛擬Dom已經(jīng)創(chuàng)建完成,即將開始渲染。在此時(shí)也可以對(duì)數(shù)據(jù)進(jìn)行更改,不會(huì)觸發(fā)updated。

4.mounted

這個(gè)鉤子在掛載完成后發(fā)生,在當(dāng)前階段,真實(shí)的Dom掛載完畢,數(shù)據(jù)完成雙向綁定,可以訪問到Dom節(jié)點(diǎn),使用$ref屬性對(duì)Dom進(jìn)行操作。也可以向后臺(tái)發(fā)送請(qǐng)求,拿到返回?cái)?shù)據(jù)。

5.beforeUpdate

這個(gè)鉤子發(fā)生在更新之前,也就是響應(yīng)式數(shù)據(jù)發(fā)生更新,虛擬dom重新渲染之前被觸發(fā),你可以在當(dāng)前階段進(jìn)行更改數(shù)據(jù),不會(huì)造成重渲染。

6.updated

這個(gè)鉤子發(fā)生在更新完成之后,當(dāng)前階段組件Dom已完成更新。要注意的是避免在此期間更改數(shù)據(jù),因?yàn)檫@可能會(huì)導(dǎo)致無限循環(huán)的更新。

7.beforeDestroy

這個(gè)鉤子發(fā)生在實(shí)例銷毀之前,在當(dāng)前階段實(shí)例完全可以被使用,我們可以在這時(shí)進(jìn)行善后收尾工作,比如清除計(jì)時(shí)器。

8.destroyed

這個(gè)鉤子發(fā)生在實(shí)例銷毀之后,這個(gè)時(shí)候只剩下了dom空殼。組件已被拆解,數(shù)據(jù)綁定被卸除,監(jiān)聽被移出,子實(shí)例也統(tǒng)統(tǒng)被銷毀。

注意點(diǎn)

在使用生命周期時(shí)有幾點(diǎn)注意事項(xiàng)需要我們牢記。

1.除了beforeCreate和created鉤子之外,其他鉤子均在服務(wù)器端渲染期間不被調(diào)用。

2.上文曾提到過,在updated的時(shí)候千萬不要去修改data里面賦值的數(shù)據(jù),否則會(huì)導(dǎo)致死循環(huán)。

3.Vue的所有生命周期函數(shù)都是自動(dòng)綁定到this的上下文上。所以,你這里使用箭頭函數(shù)的話,就會(huì)出現(xiàn)this指向的父級(jí)作用域,就會(huì)報(bào)錯(cuò)。原因下面源碼部分會(huì)講解。

源碼解讀

因?yàn)閂ue的源碼部分包含很多內(nèi)容,本文只選取生命周期相關(guān)的關(guān)鍵性代碼進(jìn)行解析。同時(shí)也強(qiáng)烈推薦大家學(xué)習(xí)Vue源碼的其他內(nèi)容,因?yàn)檫@個(gè)框架真的很優(yōu)秀,附上鏈接[Vue.js技術(shù)揭秘](https://ustbhuangyi.github.io/vue-analysis/prepare/)。

我們先來從源碼中來解答上文注意點(diǎn)的第四個(gè)問題(以下所有代碼都有刪減,用…代替刪減部分)。

// src/core/instance/lifecycle.js
// callhook 函數(shù)的功能就是在當(dāng)前vue組件實(shí)例中,調(diào)用某個(gè)生命周期鉤子注冊(cè)的所有回調(diào)函數(shù)。
// vm:Vue實(shí)例
// hook:生命周期名字
export function callHook (vm: Component, hook: string) {
  pushTarget()
  const handlers = vm.$options[hook] 
  // 初始化合并 options 的過程 、,將各個(gè)生命周期函數(shù)合并到 options 里
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

// src/core/util/error.js
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

我們從上面的代碼中可以看到callHook中調(diào)用了invokeWithErrorHandling方法,在invokeWithErrorHandling方法中,使用了apply和call改變了this指向,而在箭頭函數(shù)中this指向是無法改變的,所以我們?cè)诰帉懮芷诤瘮?shù)的時(shí)候不能使用箭頭函數(shù)。

解答完上面遺留的問題后,我們?cè)賮碇鹨恢v解各個(gè)生命周期。

1.beforeCreate和created

// src/core/instance/init
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    // 合并選項(xiàng)部分已省略

    initLifecycle(vm)  
    // 主要就是給vm對(duì)象添加了 $parent、$root、$children 屬性,以及一些其它的生命周期相關(guān)的標(biāo)識(shí)
    initEvents(vm) // 初始化事件相關(guān)的屬性
    initRender(vm)  // vm 添加了一些虛擬 dom、slot 等相關(guān)的屬性和方法
    callHook(vm, 'beforeCreate')  // 調(diào)用 beforeCreate 鉤子
    //下面 initInjections(vm) 和 initProvide(vm) 兩個(gè)配套使用,用于將父組件 _provided 中定義的值,通過 inject 注入到子組件,且這些屬性不會(huì)被觀察
    initInjections(vm) 
    initState(vm)   // props、methods、data、watch、computed等數(shù)據(jù)初始化
    initProvide(vm) 
    callHook(vm, 'created')  // 調(diào)用 created 鉤子
  }
}

// src/core/instance/state
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

我們可以看到beforeCreate鉤子調(diào)用是在initState之前的,而從上面的第二段代碼我們可以看出initState的作用是對(duì)props、methods、data、computed、watch等屬性做初始化處理。

通過閱讀源碼,我們更加清楚的明白了在beforeCreate鉤子的時(shí)候我們沒有對(duì)props、methods、data、computed、watch上的數(shù)據(jù)的訪問權(quán)限。在created中才可以。

2.beforeMount和mounted

// mountComponent 核心就是先實(shí)例化一個(gè)渲染W(wǎng)atcher
// 在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent 方法
// 兩個(gè)核心方法 vm._render(生成虛擬Dom) 和 vm._update(映射到真實(shí)Dom)
// src/core/instance/lifecycle
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    ...
  }
  callHook(vm, 'beforeMount')  // 調(diào)用 beforeMount 鉤子

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
    // 將虛擬 Dom 映射到真實(shí) Dom 的函數(shù)。
    // vm._update 之前會(huì)先調(diào)用 vm._render() 函數(shù)渲染 VNode
      ...
      const vnode = vm._render()
      ...
      vm._update(vnode, hydrating)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判斷是否 mouted 完成 并且沒有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')  //調(diào)用 mounted 鉤子
  }
  return vm
}

通過上面的代碼,我們可以看出在執(zhí)行vm._render()函數(shù)渲染VNode之前,執(zhí)行了 beforeMount鉤子函數(shù),在執(zhí)行完 vm._update()把VNode patch到真實(shí)Dom后,執(zhí)行 mouted鉤子。也就明白了為什么直到mounted階段才名正言順的拿到了Dom。

3.beforeUpdate和updated

  // src/core/instance/lifecycle
 new Watcher(vm, updateComponent, noop, {
    before () {
     // 先判斷是否 mouted 完成 并且沒有被 destroyed
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')  // 調(diào)用 beforeUpdate 鉤子
      }
    }
  }, true /* isRenderWatcher */)

 // src/core/observer/scheduler 
 function callUpdatedHooks (queue) {
   let i = queue.length
   while (i--) {
     const watcher = queue[i]
     const vm = watcher.vm
     if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
       // 只有滿足當(dāng)前 watcher 為 vm._watcher(也就是當(dāng)前的渲染watcher)
       // 以及組件已經(jīng) mounted 并且沒有被 destroyed 才會(huì)執(zhí)行 updated 鉤子函數(shù)。
       callHook(vm, 'updated')  // 調(diào)用 updated 鉤子
       }
     }
   }

第一段代碼就是在beforeMount和mounted鉤子中間出現(xiàn)的,那么watcher中究竟做了些什么呢?第二段代碼的callUpdatedHooks函數(shù)中什么時(shí)候才可以滿足條件并執(zhí)行updated呢?我們來接著往下看。

// src/instance/observer/watcher.js
export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    // 在它的構(gòu)造函數(shù)里會(huì)判斷 isRenderWatcher,
    // 接著把當(dāng)前 watcher 的實(shí)例賦值給 vm._watcher
    isRenderWatcher?: boolean
  ) {
    // 還把當(dāng)前 wathcer 實(shí)例 push 到 vm._watchers 中,
    // vm._watcher 是專門用來監(jiān)聽 vm 上數(shù)據(jù)變化然后重新渲染的,
    // 所以它是一個(gè)渲染相關(guān)的 watcher,因此在 callUpdatedHooks 函數(shù)中,
    // 只有 vm._watcher 的回調(diào)執(zhí)行完畢后,才會(huì)執(zhí)行 updated 鉤子函數(shù)
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    ...
}

看到這里我們明白了Vue是通過watcher來監(jiān)聽實(shí)例上的數(shù)據(jù)變化,進(jìn)而控制渲染流程。

4.beforeDestroy和destroyed

  // src/core/instance/lifecycle.js
  // 在 $destroy 的執(zhí)行過程中,它會(huì)執(zhí)行 vm.__patch__(vm._vnode, null)
  // 觸發(fā)它子組件的銷毀鉤子函數(shù),這樣一層層的遞歸調(diào)用,
  // 所以 destroy 鉤子函數(shù)執(zhí)行順序是先子后父,和 mounted 過程一樣。
  Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy')  // 調(diào)用 beforeDestroy 鉤子
    vm._isBeingDestroyed = true
    // 一些銷毀工作
    const parent = vm.$parent
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // 拆卸 watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    ...
    vm._isDestroyed = true
    // 調(diào)用當(dāng)前 rendered tree 上的 destroy 鉤子
    // 發(fā)現(xiàn)子組件,會(huì)先去銷毀子組件
    vm.__patch__(vm._vnode, null)
    callHook(vm, 'destroyed')  // 調(diào)用 destroyed 鉤子
    // 關(guān)閉所有實(shí)例偵聽器。
    vm.$off()
    // 刪除 __vue__ 引用
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 釋放循環(huán)引用
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}

通過上面的代碼,我們了解了組件銷毀階段的拆卸過程,其中會(huì)執(zhí)行一個(gè)__patch__函數(shù),講解起來篇幅較多,想要深入了解該部分的同學(xué)可以自行閱讀源碼解讀處給大家的鏈接。

除了這八種鉤子外,我們?cè)诠倬W(wǎng)也可以查閱到另外幾種不常用的鉤子,這里列舉出來。

幾種不常用的鉤子

activated

keep-alive 組件激活時(shí)調(diào)用,該鉤子在服務(wù)器端渲染期間不被調(diào)用。

deactivated

keep-alive 組件停用時(shí)調(diào)用,該鉤子在服務(wù)器端渲染期間不被調(diào)用。

errorCaptured

當(dāng)捕獲一個(gè)來自子孫組件的錯(cuò)誤時(shí)被調(diào)用。此鉤子會(huì)收到三個(gè)參數(shù):錯(cuò)誤對(duì)象、發(fā)生錯(cuò)誤的組件實(shí)例以及一個(gè)包含錯(cuò)誤來源信息的字符串。此鉤子可以返回 false 以阻止該錯(cuò)誤繼續(xù)向上傳播

你可以在此鉤子中修改組件的狀態(tài)。因此在模板或渲染函數(shù)中設(shè)置其它內(nèi)容的短路條件非常重要,它可以防止當(dāng)一個(gè)錯(cuò)誤被捕獲時(shí)該組件進(jìn)入一個(gè)無限的渲染循環(huán)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 使用Vue在日常開發(fā)中會(huì)頻繁接觸和使用生命周期,在官方文檔中是這么解釋生命周期的: 每個(gè) Vue 實(shí)例在被創(chuàng)...
    心_c2a2閱讀 2,381評(píng)論 1 8
  • 使用Vue開發(fā)對(duì)于Vue生命周期的理解自然少不了,以前在面試時(shí)候也被問到過,當(dāng)時(shí)也就只能回答出生命周期的幾個(gè)鉤子函...
    Mstian閱讀 404評(píng)論 0 3
  • 學(xué)習(xí)主線:從vue2生命周期圖出發(fā),找出背后的源碼實(shí)現(xiàn),來探索vue成長(zhǎng)之路! [TOC] 生命周期圖 vue2....
    scrollHeart閱讀 1,061評(píng)論 0 0
  • 春節(jié)繼續(xù)寫博客~加油! 這次來學(xué)習(xí)一下Vue的生命周期,看看生命周期是怎么回事。 callHook 生命周期主要就...
    VioletJack閱讀 850評(píng)論 0 2
  • 因?yàn)樽罱覀兘M內(nèi)有個(gè)分享主題,即vue2的源碼學(xué)習(xí)分享,我們幾個(gè)人分別分享幾個(gè)不同部分,但是雖然我們的分工是每個(gè)人...
    阿go閱讀 1,835評(píng)論 0 0

友情鏈接更多精彩內(nèi)容