在 Vue 中,除函數式組件外,全部組件都是 Vue 實例。每一個 Vue 實例在被建立時都要通過一系列的初始化過程:數據監聽、編譯模板、將實例掛載到 DOM 並在數據變化時更新 DOM 等。
在生成 Vue 實例的過程當中會運行一些叫作生命週期鉤子的函數,這給了用戶在不一樣階段添加本身的代碼的機會。本文從源碼的角度來詳細闡述組件生命週期的相關內容。
前端
生命週期鉤子函數調用是經過 callHook 函數完成的,callHook 函數主要包含三個方面的內容。
vue
function callHook (vm, hook) {
pushTarget()
const handlers = vm.$options[hook]
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()
}
複製代碼
在生成 Vue 實例的過程當中會調用 mergeOptions 函數對選項進行處理,生命週期鉤子函數通過合併處理後會添加到實例對象的 $options 屬性上,合併後各生命週期函數存儲在對應的數組中。具體細節可參看文章《選項合併》。
callHook 函數調用的形式以下所示:
node
// 調用 created 生命週期鉤子函數
callHook(vm, 'created')
複製代碼
此時 callHook 函數會循環遍歷執行 vm.$options.created 數組中的函數,以完成 created 生命週期鉤子函數的調用。
vue-router
在函數首尾有以下代碼:
後端
function callHook (vm, hook) {
pushTarget()
/* 省略... */
popTarget()
}
複製代碼
這兩個函數的源碼以下所示:
數組
Dep.target = null
const targetStack = []
function pushTarget (target) {
targetStack.push(target)
Dep.target = target
}
function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
複製代碼
Vue 實例的當前觀察者對象是惟一的,所謂當前觀察者對象是指即將要收集依賴的目標,pushTarget 函數將觀察者對象入棧而不是簡單的賦值,是爲了在當前觀察者對象操做完成後恢復成以前的觀察者對象。
在函數的首尾調用 pushTarget() 和 popTarget() 函數,是爲了防止在生命週期鉤子函數中使用 props 數據時收集冗餘的依賴。具體詳情可參看《響應式原理》。
緩存
在 callHook 函數中還有一部分代碼:
ide
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
複製代碼
這行代碼比較有意思,也就是說在執行生命週期鉤子函數時,若是 vm._hasHookEvent 的值爲 true,則會額外觸發一個形如 hook:created 的事件。
那麼何時實例的 _hasHookEvent 屬性值爲真呢?還記得在上篇文章講解 $on 方式時有提過這點:
函數
const hookRE = /^hook:/
Vue.prototype.$on = function(event, fn){
/* 省略... */
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
/* 省略... */
}
複製代碼
上篇文章同時講到,在組件上使用自定義指令最終會轉化成調用 $on 的形式,也就是說按照如下使用就能命中這種狀況:
oop
<Child @hook:created = "doSomething"></Child>
複製代碼
這種形式的事件稱爲 hookEvent,在官方文檔上沒有找到 hookEvent 的說明,可是在 Vue 源碼中有實現。所謂 hookEvent 就是特殊命名的事件—— hook: + 生命週期名稱。這種事件會在子組件對應生命週期鉤子函數調用時被調用。
那 hookEvent 有什麼用呢?其實在使用第三方組件的時候可以用到,使用 hookEvent 能夠在不破壞第三方組件代碼的前提下,向其注入生命週期函數。
關於組件實例的生命週期,官網上面有一張很經典的圖片:
function Vue (options) {
/* 省略警告信息 */
this._init(options)
}
複製代碼
_init 方法首先進行合併選項,而後初始化生命週期、事件等,最後掛載 DOM 元素。代碼以下所示:
Vue.prototype._init = function (options) {
const vm = this
/*...*/
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/*...*/
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
/*...*/
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製代碼
這裏函數調用的順序很重要,數據的處理都是在 beforeCreate 生命週期函數調用以後初始化的,也就是說在 beforeCreate 生命週期函數中,不能使用 props、methods、data、computed 和 watch 等數據,也不能使用 provide/inject 中的數據。通常從後端加載數據不用賦值給data中時,能夠放在這個生命週期中。
在 beforeCreate 與 created 生命週期函數調用中間,調用初始化各個數據的函數。initState 函數代碼以下所示:
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)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
複製代碼
注意 initState 函數中函數的調用順序:initProps——initData——initComputed——initWatch。這樣初始化順序的結果是在 data 選項中可使用 props;在 computed 選項中可使用 data、props 中的數據;watch 選項能夠監聽 data、props、computed 數據的變化。methods 選項的組成是函數,在函數調用時這些初始化工做已經完成,因此可使用所有的數據。
初始化 inject 的 initInjections 函數在 initState 以前調用,最後調用初始化 provide 的 initProvide 函數。這樣就決定了在 data、props、computed 等選項中可使用 inject 中的數據,provide 選項中可使用 data、props、computed、inject 等的數據。
調用 created 生命週期函數以前,數據初始化已經完成,在函數中能夠操做這些數據。向後端請求的數據須要賦值給 data 時,能夠放在 created 生命週期函數中。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el,hydrating){
el = el && query(el)
/* 省略... */
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* 省略... */
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
/* 省略... */
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
/* 省略... */
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
/* 省略... */
}
}
return mount.call(this, el, hydrating)
}
複製代碼
該函數的做用是將 template/el 轉化成渲染函數,具體的轉化過程可參看《模板編譯》一文。
根據渲染函數完成掛載的代碼以下所示:
Vue.prototype.$mount = function (el,hydrating){
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
function mountComponent (vm,el,hydrating){
vm.$el = el
/* 省略渲染函數不存在的警告信息 */
callHook(vm, 'beforeMount')
let updateComponent
/* 刪除性能埋點相關 */
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
複製代碼
能夠看到 beforeMount 生命週期是在渲染函數生成以後、開始執行掛載以前調用的。beforeMount 鉤子函數執行後,實例化一個渲染函數觀察者對象,關於 Watcher 相關內容能夠參看《響應式原理》。
從渲染函數到生成真實DOM的過程由 updateComponent 函數來完成,其中 _render 函數的做用是根據渲染函數生成 VNode,_update 函數的做用是根據 VNode 生成真實DOM並插入到對應位置中。
在掛載完成後,會調用 mounted 生命週期鉤子函數,在該生命週期內能夠對DOM進行操做。
這裏有個判斷條件:vm.$vnode == null,組件初始化的時候 $vnode 不爲空,當條件成立時,說明是經過 new Vue() 來進行初始化的。換而言之,組件初始化時,不會在此處執行 mounted 生命週期鉤子函數,那麼組件 mounted 生命週期函數在何處調用呢?
_update 函數本質上是經過調用 patch 函數來完成真實DOM元素的生成與插入,在 patch 函數的最後有以下代碼:
function patch (oldVnode,vnode,hydrating,removeOnly){
/* 省略... */
invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch)
return vnode.elm
}
function invokeInsertHook (vnode, queue, initial) {
/* 省略... */
for (let i = 0; i < queue.length; ++i) {
queue[i].data.hook.insert(queue[i])
}
}
function insert (vnode) {
const { context, componentInstance } = vnode
if (!componentInstance._isMounted) {
componentInstance._isMounted = true
callHook(componentInstance, 'mounted')
}
/* 省略 keep-alive 相關...*/
}
複製代碼
能夠看到組件 mounted 生命週期鉤子函數的調用是在 patch 的最後階段進行的,另外 insertedVnodeQueue 是一個 VNode 數組,數組中 VNode 的順序是子 VNode 在前,父 VNode 在後,所以 mounted 鉤子函數的執行順序也是子組件先執行,父組件後執行。
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true)
複製代碼
Watcher 構造函數代碼以下所示:
class Watcher {
constructor (vm,expOrFn,cb,options,isRenderWatcher) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
if (options) {
/* 省略... */
this.before = options.before
}
}
/* 省略... */
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/* 省略... */
}
複製代碼
能夠看到在實例化渲染函數觀察者對象時,會將傳入的 before 函數添加到觀察者對象上。在數據更新時會執行 update 方法,在沒有添增強制要求時,默認執行 queueWatcher 函數完成數據更新。
export function queueWatcher (watcher: Watcher) {
/* 省略... */
flushSchedulerQueue()
/* 省略... */
}
function flushSchedulerQueue () {
/* 省略... */
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
watcher.run()
/* 省略... */
}
callUpdatedHooks(updatedQueue)
/* 省略... */
}
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) {
callHook(vm, 'updated')
}
}
}
複製代碼
數據更新是經過觀察者對象的實例方法 run 完成的,從上代碼能夠看到:在數據更新前會調用實例對象上的 before 方法,從而執行 beforeUpdate 生命週期鉤子函數;在數據更新完成後,經過執行 callUpdatedHooks 函數完成 updated 生命週期函數的調用。
Vue.prototype.$destroy = function () {
const vm = this
if (vm._isBeingDestroyed) { return }
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract){
remove(parent.$children, vm)
}
if (vm._watcher){
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--){
vm._watchers[i].teardown()
}
if (vm._data.__ob__){
vm._data.__ob__.vmCount--
}
vm._isDestroyed = true
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed')
vm.$off()
if (vm.$el) {
vm.$el.__vue__ = null
}
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
複製代碼
首先判斷實例上 _isBeingDestroyed 是否爲 true,這是實例正在被銷燬的標識,爲了防止重複銷燬組件。當正式開始執行銷燬邏輯以前,調用 beforeDestroy 生命週期鉤子函數。
銷燬組件的具體步驟有:
一、將實例從其父級實例中刪除。
二、移除實例的依賴。
三、移除實例內響應式數據的引用。
四、刪除子組件實例。
完成上述操做後調用 destroyed 生命週期鉤子函數,而後移除實例上的所有事件監聽器。
當組件被 keep-alive 內置組件包裹時,組件實例會被緩存起來。這些組件在首次渲染時各生命週期與普通組件同樣,再次渲染時 created、mounted 等鉤子函數就再也不生效。
被 keep-alive 包裹的組件被緩存以後有兩個獨有的生命週期: activated 和 deactivated。activated 生命週期在組件激活時調用、deactivated 生命週期在組件停用時調用。
上一節講 mounted 生命週期時說過,組件的 mounted 的生命週期鉤子函數是在 insert 方法中調用的。當時將函數中對 keep-alive 的處理省略了,這裏重點闡述。
insert (vnode) {
/* 省略... */
if (vnode.data.keepAlive) {
if (context._isMounted) {
queueActivatedComponent(componentInstance)
} else {
activateChildComponent(componentInstance, true)
}
}
}
複製代碼
queueActivatedComponent 函數的調用是爲了修復 vue-router 中的一個問題:在更新過程當中 keep-alive 的子組件可能會發生改變,直接遍歷樹結構可能會調用錯誤子組件實例的 activated 生命週期鉤子函數,所以這裏不作處理而是將組件實例放入隊列中,等 patch 過程結束後再作處理。
queueActivatedComponent 最終也是調用 activateChildComponent 函數來執行 activated 生命週期鉤子函數。
function activateChildComponent(vm,direct){
/* 省略... */
if (vm._inactive || vm._inactive === null) {
vm._inactive = false
for (let i = 0; i < vm.$children.length; i++) {
activateChildComponent(vm.$children[i])
}
callHook(vm, 'activated')
}
}
複製代碼
能夠看到就是在這裏調用的 activated 生命週期鉤子函數,而且會遞歸調用所有子組件的 activated 生命週期鉤子函數。
deactivated 生命週期是在 destroy 鉤子函數中調用的:
destroy (vnode) {
const { componentInstance } = vnode
if (!componentInstance._isDestroyed) {
if (!vnode.data.keepAlive) {
componentInstance.$destroy()
} else {
deactivateChildComponent(componentInstance, true)
}
}
}
複製代碼
keep-alive 的子組件的子組件會走 else 分支,直接調用 deactivateChildComponent 函數。
function deactivateChildComponent(vm, direct){
/* 省略... */
if (!vm._inactive) {
vm._inactive = true
for (let i = 0; i < vm.$children.length; i++) {
deactivateChildComponent(vm.$children[i])
}
callHook(vm, 'deactivated')
}
}
複製代碼
在該函數中,會調用的 deactivated 生命週期鉤子函數,而且會遞歸調用所有子組件的 deactivated 生命週期鉤子函數。
生命週期鉤子函數的是經過 callHook 來調用的,該函數不只遍歷執行對應的生命週期函數,還能防止收集冗餘依賴和觸發 hookEvent 事件。hookEvent 可以非侵入的向一個組件注入生命週期函數。
經過 new Vue() 實例化 Vue 對象會調用 _init 方法完成一系列初始化操做,在初始化數據以前會調用 beforeCreate 鉤子,在數據初始化後調用 created 鉤子。在生成渲染函數以後,調用 beforeMount 鉤子,接着根據渲染函數生成真實DOM並掛載,而後調用 mounted 鉤子。數據更新時,在從新渲染以前調用 beforeUpdate 鉤子,在完成渲染後調用 updated 鉤子。在調用實例方法 $destroy() 銷燬實例時首先調用 beforeDestroy 鉤子,而後執行銷燬操做,最後調用 destroyed 鉤子。
被 keep-alive 緩存起來的組件被激活時會調用 activated 鉤子,在 patch 最後階段的 insert 鉤子函數中執行。組件停用時調用 deactivated 鉤子,在 patch 的 destroy 鉤子函數中執行。
歡迎關注公衆號:前端桃花源,互相交流學習!