讀讀 Vue 的原始碼:生命週期
TL;DR
在 Vue 裡面的:
beforeCreate => created => beforeMount => mounted => beforeUpdate => updated => beforeDestroy => destroyed
等於:
// From: src/core/index.js
Vue => initMixin => $mount => mountComponent => Watcher => $destroy
而分隔這些階段的關鍵(也就是 =>
)是 callHook
函式。
序言
這幾天想知道原生 JavaScript 的生命週期到底是怎麼搞的,所以我立刻想到讀 Vue 的原始碼,來看看 Vue 到底是用什麼原生 API 呼叫。
但我最後發現 Vue 的生命週期與完全原生 JavaScript 完全不一樣,不過看了原始碼也收穫頗豐啦。
複習一下生命週期
Vue 的生命週期有四個階段。加上階段的前與後,總共有八個階段:
before | after | |
---|---|---|
create | beforeCreate | created |
mount | beforeMount | mounted |
update | beforeUpdate | updated |
destroy | beforeDestroy | destroyed |
在載入必要的生命週期與事件後,四個生命週期的發動時機分別是:
- create:載入注入(injection)、內部狀態與響應模式的前後。
- mount:繪製 HTML template 的前後。
- update:組件內部狀態更新的前後。
- destroy:組件觀察器(watcher)與事件被銷毀的前後。
啟程:Instance 的五行函式
先從 src/core/index.js
開始。這個檔案會帶我們進到 src/core/instance/index.js
檔案。跳進去以後,你會發現 Vue 執行了五個函式:
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
比較重要的就 lifecycleMixin
與 initMixin
。這兩個以外的函式,都是初始化各種變數。你可能覺得我應該會先從 lifecycleMixin
開始講——但我要先講的是 initMixin
這個函式:從這裡接入,才能知道 Vue 的生命旅程。
開始吧。
揭露真實的自我:initMixin 做了啥
initMixin
這函式大約六十行左右。裡面有不少是針對環境不同所給出的措施。所以你看連伊斯坦堡都不鳥它了。
重點是後面的程式碼:
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
這段程式把整個過程都解釋得很清楚:在一切開始之前,先執行 initLifecycle
initEvents
initRender
這三個函式。initLifecycle
很簡單,就是啟動組件的變數:
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
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 = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
雖然我不懂為什麼這函式會叫「初始化生命週期」。
接著 initEvents
會讓組件開始添加監聽器。監聽器的內部運作……頗複雜的,所以暫時不看了。在 src/core/instance/events.js
還不夠、還需要載入 src/core/vdom/helpers/update-listeners.js
啥的……
initRender
會透過 Virtual DOM 來建立組件的元素、同時藉由 defineReactive
開始載入 Vue 的響應式功能:
// src/core/instance/render.js
/* 略 */
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
/* 略 */
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
/* 略 */
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
!isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
}, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
!isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
}, true)
} else {
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
附帶一題,Vue 的響應式功能可以從 src/core/observer/index.js
看到,裡面是由 getOwnPropertyDescriptor
與 defineProperty
完成的。
完成後,就開始呼叫 callHook
了。callHook
會呼叫對應的事件監聽器。所以這就是為什麼在 Vue 裡面寫了 beforeCreate
就能執行裡面的程式。
然後從 callHook(vm, 'beforeCreate')
到 callHook(vm, 'created')
之間又執行了 initInjections
initState
initProvide
三個函式。
- 位於 src/core/instance/inject.js 的
initInjections
其實也是去呼叫前面提到的initRender
- 位於
src/core/instance/state.js
的initState
則會初始化所有有寫到的 instance 像是props
,methods
,data
之類的。不過computed
不會初始化。 - 位於
src/core/instance/inject.js
的initProvide
則與 inject API 有關。不過我沒用過所以不清楚。
然後 $mount
函式會把我們帶到下個階段。
直到現在才把 create 階段講完。不好了不好了。不過現在已經發現一個新的函式:callHook
了。我們可以用這函式當各階段的分界點。
風塵僕僕的旅途:mountComponent
那麼 $mount
又會去哪裡?我只好用搜尋的,搜看看 $mount
去哪裡了。按照平台不同,也所差異。但最後都會指向一個稱作 mountComponent
的函式。這函式又位於 src/core/instance/lifecycle.js
那邊。
欸……src/core/instance/lifecycle.js
?那我們不就回到了生命週期嗎?好欸!快看看 mountComponent
做了啥吧!
mountComponent
主要做兩件事:把渲染的組件更新到 Virtual DOM 裡面、還有添加 update 階段所需的 Watcher
類別。你看裡面都有 callHook(vm, 'beforeUpdate')
了。
為什麼要加在這裡?
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
看起來與 $forceUpdate
有關啊……但這也就是說,你如果要更新,就只要更新裡面的變數/狀態就好。
峰迴路轉:Watcher 怎麼這麼多方法
Watcher
這個類別在 src/core/observer/watcher.js
那邊。裡面有很多方法。但其中最重要的方法,就是 update
。
但 callHook(vm, 'updated')
又跑去哪裡了呢?
update
方法會呼叫src/core/observer/scheduler.js
裡面的queueWatcher
函式queueWatcher
又會呼叫flushSchedulerQueue
flushSchedulerQueue
則在一段優化與更新後呼叫callUpdatedHooks
。callUpdatedHooks
在最後終於呼叫了callHook(vm, 'updated')
。
另外 Watcher
那段,我實際上的追蹤流程,整個是反過來的。
我看到有個組件落在地,看它的生命似已到盡頭:$destroy 怎麼跑的
那麼 callHook(vm, 'beforeDestroy') 又在哪裡?
在 src/core/instance/lifecycle.js
的 lifecycleMixin
那裡。
lifecycleMixin
註冊了一個稱作 $destroy
的 prototype
。$destroy
裡面瑣碎的程式碼太多,就用註解來幫我講好了:
// src/core/instance/lifecycle.js
// 省略
callHook(vm, 'beforeDestroy')
// remove self from parent
// teardown watchers
// remove reference from data ob
// frozen object may not have observer.
// call the last hook...
// invoke destroy hooks on current rendered tree
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
// remove __vue__ reference
// release circular reference (#6759)
這樣,整個組件的歷程就結束了。文章也接近尾聲。
結語
雖然我到現在都還不知道原生 JavaScript 的歷程。但是現在,我們已經對 Vue 的生命週期,已經有更深理解了。
可是我好累……