Vue原始碼探究-生命週期

jylzs369發表於2019-02-19

本篇程式碼位於vue/src/core/instance/lifecycle.js

初步探索完了核心類的實現之後,接下來就要開始深入到Vue實現的具體功能部分了。在所有的功能開始執行之前,要來理解一下Vue的生命週期,在初始化函式中所有功能模組繫結到Vue的核心類上之前,最先開始執行了一個初始化生命週期的函式initLifecycle(vm),先來看看這個函式做了些什麼。

生命週期初始化屬性

// 匯出initLifecycle函式,接受一個Component型別的vm引數
export function initLifecycle (vm: Component) {
  // 獲取例項的$options屬性,賦值為options變數
  const options = vm.$options

 // 找到最上層非抽象父級 
  // locate first non-abstract parent
  // 首先找到第一個父級
  let parent = options.parent
  // 判斷是否存在且非抽象
  if (parent && !options.abstract) {
    // 遍歷尋找最外層的非抽象父級
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    // 將例項新增到最外層非抽象父級的子元件中
    parent.$children.push(vm)
  }

  // 初始化例項的公共屬性
  // 設定父級屬性,如果之前的程式碼未找到父級,則vm.$parent為undefined
  vm.$parent = parent
  // 設定根屬性,沒有父級則為例項物件自身
  vm.$root = parent ? parent.$root : vm

  // 初始化$children和$refs屬性
  // vm.$children是子元件的陣列集合
  // vm.$refs是指定引用名稱的元件物件集合
  vm.$children = []
  vm.$refs = {}

  // 初始化一些私有屬性
  // 初始化watcher
  vm._watcher = null
  // _inactive和_directInactive是判斷啟用狀態的屬性
  vm._inactive = null
  vm._directInactive = false
  // 生命週期相關的私有屬性
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}
複製程式碼

initLifecycle 函式非常簡單明瞭,主要是在生命週期開始之前設定一些相關的屬性的初始值。一些屬性將在之後的生命週期執行期間使用到。

生命週期初始化方法

生命週期的開始除了設定了相關屬性的初始值之外,還為類原型物件掛載了一些方法,包括私有的更新元件的方法和公用的生命週期相關的方法。這些方法都包含在 lifecycleMixin 函式中,還記得這也是在定義核心類之後執行的那些函式之一,也來看看它的內容。

// 匯出lifecycleMixin函式,接收形參Vue,
// 使用Flow進行靜態型別檢查指定為Component類
export function lifecycleMixin (Vue: Class<Component>) {
  // 為Vue原型物件掛載_update私有方法
  // 接收vnode虛擬節點型別引數和一個可選的布林值hydrating
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    // 定義例項變數
    const vm: Component = this
    
    // 下面三條賦值操作主要是為了儲存屬性
    // 例項的$el屬性賦值給prevEl變數,這是新傳入的例項掛載元素
    const prevEl = vm.$el
    // 例項的_vnode屬性賦值給prevVnode變數,儲存的舊虛擬節點
    const prevVnode = vm._vnode
    // 將activeInstance賦值給prevActiveInstance變數,啟用例項
    // activeInstance初始為null
    const prevActiveInstance = activeInstance

    // 下面是針對新屬性的賦值
    // 將新例項設定為activeInstance
    activeInstance = vm
    // 將傳入的vnode賦值給例項的_vnode屬性
    // vnode是新生成的虛擬節點數,這裡把它儲存起來覆蓋
    vm._vnode = vnode
    // 下面使用到的Vue.prototype .__ patch__方法是在執行時裡注入的
    // 根據執行平臺的不同定義
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // 如果prevVnode屬性不存在說明是新建立例項
    // 執行例項屬性$el的初始化渲染,否則更新節點
    if (!prevVnode) {
      // 如果舊的虛擬節點不存在則呼叫patch方法
      // 傳入掛載的真實DOM節點和新生成的虛擬節點
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 否則執行虛擬節點更新操作,傳入的是新舊虛擬節點
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }

    // 將之前的啟用例項又賦值給activeInstance
    activeInstance = prevActiveInstance
    // 更新__vue__屬性的引用
    // update __vue__ reference
    // 如果存在舊元素則設定它的__vue__引用為null
    if (prevEl) {
      prevEl.__vue__ = null
    }
    // 如果例項的$el屬性存在,設定它的__vue__引用為該例項
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // 如果父節點是一個高階元件,也更新它的元素節點
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // 更新的鉤子由排程器呼叫,以確保在父更新的鉤子中更新子項。
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

  // 為Vue例項掛載$forceUpdate方法,實現強制更新
  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  // 為Vue例項掛載$destroy方法
  Vue.prototype.$destroy = function () {
    // 定義例項變數
    const vm: Component = this
    // 如果例項已經在銷燬中,則返回
    if (vm._isBeingDestroyed) {
      return
    }
    // 呼叫beforeDestroy鉤子
    callHook(vm, 'beforeDestroy')
    // 給例項設定正在銷燬中的標誌
    vm._isBeingDestroyed = true
    // 從父元件中移除自身
    // remove self from parent
    const parent = vm.$parent
    // 如果非抽象父級元件存在且沒有在銷燬中,則從父元件中移除例項
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm)
    }
    // 銷燬所有觀察器
    // teardown watchers
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // 移除物件引用
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // 呼叫最後的鉤子
    // call the last hook...
    // 設定例項的已銷燬標誌
    vm._isDestroyed = true
    // 呼叫當前渲染樹上的銷燬鉤子
    // invoke destroy hooks on current rendered tree
    vm.__patch__(vm._vnode, null)
    // 觸發銷燬鉤子
    // fire destroyed hook
    callHook(vm, 'destroyed')
    // turn off all instance listeners.
    // 清除所有監聽事件
    vm.$off()
    // 移除例項引用
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null
    }
    // 釋放迴圈引用
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null
    }
  }
}
複製程式碼

lifecycleMixin 函式實現了三個原型繼承方法:

私有方法 _update

這個函式用於更新元件,實現資料和元素節點的無重新整理更新,涉及到虛擬節點相關的一些內容,具體實現留給未來研究虛擬節點和資料更新時再深入探索。

公用方法 $forceUpdate

實現元件強制重新整理,這個方法是從例項上設定的watcher物件方法中引用而來,在生命週期初始化的時候為例項設定了一個私有的_watcher屬性,在觀察者系統的功能模組中具體實現了這一物件,也放到以後在去深入瞭解。這裡只要知道可以呼叫這個共有的API實現手動更新元件。

公用方法 $destroy

例項銷燬方法。在剛開始討論生命週期的開啟時,就瞭解到了這個銷燬Vue例項元件的方法,凡事都有始有終,從這裡可以明白無誤的認識到,Vue例項是一個生命過程。那麼在Vue的生命過程中有哪些重要的階段,是接下來要繼續探索的內容。

生命週期過程

最明白無誤的生命週期過程在官方文件中有介紹,這裡再貼上這張經典的圖示來做個紀念。

生命週期圖示

生命週期鉤子

對照生命週期圖示中呈現的各種鉤子函式,從原始碼總結了他們的呼叫時機,順便又學習一遍鉤子執行的線路:

  • callHook(vm, 'beforeCreate')
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
複製程式碼

new Vue() 建立例項開始 ,在執行 _init() 方法時開始初始化了生命週期、事件和渲染。緊接著就呼叫了 beforeCreate 鉤子函式。此時與資料相關的屬性都還沒有初始化 ,所以在這個階段想要用獲取到元件的屬性是無法成功的。

  • callHook(vm, 'created')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
複製程式碼

beforeCreate 呼叫後,繼續初始化屬性注入、狀態、子元件屬性提供器。然後立即呼叫 created 鉤子,這個時候資料可訪問了,但是還沒有開始渲染頁面,適合一些資料的初始化操作。另外provide和injection主要為高階外掛/元件庫提供用例。並不推薦直接用於應用程式程式碼中,所以此刻我們主要注意的是觀察器的初始化完成。 到這一步之後,就開始進入渲染流程。

  • callHook(vm, 'beforeMount')

渲染的執行流程稍微複雜一些,例項裝載方法 $mount 是根據平臺的不同需求而分別定義的,在執行 $mount 方法的時候,開始裝載元件,具體內容在 mountComponent 函式中,在此函式的最開始時渲染虛擬節點之前就呼叫了 beforeMount 鉤子,然後開始執行 updateComponent 來渲染元件檢視。

  • callHook(vm, 'mounted')

緊接著上面檢視的渲染完成,mounted 鉤子被呼叫。在這個鉤子中還呼叫了內部的插入鉤子渲染引用的子元件,這之後就開始處於生命週期的正常運轉期。在這個時期內觀察器系統開始監控所有的資料更新,進入資料更新並重新渲染檢視的迴圈中。

  • callHook(vm, 'beforeUpdate')

在觀察器的作用下,如果有資料的更新時就會先呼叫 beforeUpdate 鉤子。

  • callHook(vm, 'updated')

當資料更新並且完成檢視渲染後呼叫 updated 鉤子。這個鉤子和上面的鉤子會一直在生命週期運轉期裡不斷被觸發。

  • callHook(vm, 'activated') 和 callHook(vm, 'deactivated')

activateddeactivated 這兩個特殊鉤子是在使用 keep-alive 元件的時候才有效。分別在元件被啟用或切換到其他元件的時候被呼叫。 使用 keep-alive 模式在切換到不同元件檢視的過程中不會進行重新載入,這就意味著其他的鉤子函式都不會被呼叫,如果在離開頁面和進入頁面的時候執行某些操作,這兩個鉤子就非常有用。

  • callHook(vm, 'beforeDestroy') 和 callHook(vm, 'destroyed')

beforeDestroydestroyed 鉤子與上面的兩個鉤子相對應,是在普通模式下會有效的鉤子。例項的生命週期的最後階段就是執行銷燬,在銷燬之前呼叫 beforeDestroy。然後清除了所有的資料引用、觀察器和事件監聽器。最後呼叫 destroyed 宣告生命週期的完全終止。


之前看過很多次Vue的生命週期圖,但在學習原始碼之前並沒有特別深的感觸,現在隨著探索原始碼的深入,終於感覺到在慢慢了解這個過程的意義。整個生命週期的構建過程並不是最難的實現部分,但它是整個架構的背後支撐力量,有了生命週期的正常運轉,才能一步步地實現接下來要學習的各種功能。

相關文章