本篇程式碼位於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')
activated
和 deactivated
這兩個特殊鉤子是在使用 keep-alive
元件的時候才有效。分別在元件被啟用或切換到其他元件的時候被呼叫。 使用 keep-alive
模式在切換到不同元件檢視的過程中不會進行重新載入,這就意味著其他的鉤子函式都不會被呼叫,如果在離開頁面和進入頁面的時候執行某些操作,這兩個鉤子就非常有用。
- callHook(vm, 'beforeDestroy') 和 callHook(vm, 'destroyed')
beforeDestroy
和 destroyed
鉤子與上面的兩個鉤子相對應,是在普通模式下會有效的鉤子。例項的生命週期的最後階段就是執行銷燬,在銷燬之前呼叫 beforeDestroy
。然後清除了所有的資料引用、觀察器和事件監聽器。最後呼叫 destroyed
宣告生命週期的完全終止。
之前看過很多次Vue的生命週期圖,但在學習原始碼之前並沒有特別深的感觸,現在隨著探索原始碼的深入,終於感覺到在慢慢了解這個過程的意義。整個生命週期的構建過程並不是最難的實現部分,但它是整個架構的背後支撐力量,有了生命週期的正常運轉,才能一步步地實現接下來要學習的各種功能。