前言
相信大家對 Vue 有哪些生命週期早就已經爛熟於心,但是對於這些生命週期的前後分別做了哪些事情,可能還有些不熟悉。
本篇文章就從一個完整的流程開始,詳細講解各個生命週期之間發生了什麼事情。
注意本文不涉及 keep-alive
的場景和錯誤處理的場景。
初始化流程
new Vue
從 new Vue(options)
開始作為入口,Vue
只是一個簡單的建構函式,內部是這樣的:
function Vue (options) {
this._init(options)
}
複製程式碼
進入了 _init
函式之後,先初始化了一些屬性,然後開始第一個生命週期:
callHook(vm, 'beforeCreate')
複製程式碼
beforeCreate被呼叫完成
beforeCreate
之後
- 初始化
inject
- 初始化
state
- 初始化
props
- 初始化
methods
- 初始化
data
- 初始化
computed
- 初始化
watch
- 初始化
- 初始化
provide
所以在 data
中可以使用 props
上的值,反過來則不行。
然後進入 created
階段:
callHook(vm, 'created')
複製程式碼
created被呼叫完成
呼叫 $mount
方法,開始掛載元件到 dom
上。
如果使用了 runtime-with-compile
版本,則會把你傳入的 template
選項,或者 html
文字,通過一系列的編譯生成 render
函式。
- 編譯這個
template
,生成ast
抽象語法樹。 - 優化這個
ast
,標記靜態節點。(渲染過程中不會變的那些節點,優化效能)。 - 根據
ast
,生成render
函式。
對應具體的程式碼就是:
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
複製程式碼
如果是腳手架搭建的專案的話,這一步 vue-cli
已經幫你做好了,所以就直接進入 mountComponent
函式。
那麼,確保有了 render
函式後,我們就可以往渲染
的步驟繼續進行了
beforeMount被呼叫完成
把 渲染元件的函式
定義好,具體程式碼是:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
複製程式碼
拆解來看,vm._render
其實就是呼叫我們上一步拿到的 render
函式生成一個 vnode
,而 vm._update
方法則會對這個 vnode
進行 patch
操作,幫我們把 vnode
通過 createElm
函式建立新節點並且渲染到 dom節點
中。
接下來就是執行這段程式碼了,是由 響應式原理
的一個核心類 Watcher
負責執行這個函式,為什麼要它來代理執行呢?因為我們需要在這段過程中去 觀察
這個函式讀取了哪些響應式資料,將來這些響應式資料更新的時候,我們需要重新執行 updateComponent
函式。
如果是更新後呼叫 updateComponent
函式的話,updateComponent
內部的 patch
就不再是初始化時候的建立節點,而是對新舊 vnode
進行 diff
,最小化的更新到 dom節點
上去。具體過程可以看我的上一篇文章:
為什麼 Vue 中不要用 index 作為 key?(diff 演算法詳解)
這一切交給 Watcher
完成:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製程式碼
注意這裡在before
屬性上定義了beforeUpdate
函式,也就是說在 Watcher
被響應式屬性的更新觸發之後,重新渲染新檢視之前,會先呼叫 beforeUpdate
生命週期。
關於 Watcher
和響應式的概念,如果你還不清楚的話,可以閱讀我之前的文章:
手把手帶你實現一個最精簡的響應式系統來學習Vue的data、computed、watch原始碼
注意,在 render
的過程中,如果遇到了 子元件
,則會呼叫 createComponent
函式。
createComponent
函式內部,會為子元件生成一個屬於自己的建構函式
,可以理解為子元件自己的 Vue
函式:
Ctor = baseCtor.extend(Ctor)
複製程式碼
在普通的場景下,其實這就是 Vue.extend
生成的建構函式,它繼承自 Vue
函式,擁有它的很多全域性屬性。
這裡插播一個知識點,除了元件有自己的生命週期
外,其實 vnode
也是有自己的 生命週期的
,只不過我們平常開發的時候是接觸不到的。
那麼子元件會有自己的 init
週期,這個週期內部會做這樣的事情:
// 建立子元件
const child = createComponentInstanceForVnode(vnode)
// 掛載到 dom 上
child.$mount(vnode.elm)
複製程式碼
而 createComponentInstanceForVnode
內部又做了什麼事呢?它會去呼叫 子元件
的建構函式。
new vnode.componentOptions.Ctor(options)
複製程式碼
建構函式的內部是這樣的:
const Sub = function VueComponent (options) {
this._init(options)
}
複製程式碼
這個 _init
其實就是我們文章開頭的那個函式,也就是說,如果遇到 子元件
,那麼就會優先開始子元件
的構建過程,也就是說,從 beforeCreated
重新開始。這是一個遞迴的構建過程。
也就是說,如果我們有 父 -> 子 -> 孫
這三個元件,那麼它們的初始化生命週期順序是這樣的:
父 beforeCreate
父 create
父 beforeMount
子 beforeCreate
子 create
子 beforeMount
孫 beforeCreate
孫 create
孫 beforeMount
孫 mounted
子 mounted
父 mounted
複製程式碼
然後,mounted
生命週期被觸發。
mounted被呼叫完成
到此為止,元件的掛載就完成了,初始化的生命週期結束。
更新流程
當一個響應式屬性被更新後,觸發了 Watcher
的回撥函式,也就是 vm._update(vm._render())
,在更新之前,會先呼叫剛才在 before
屬性上定義的函式,也就是
callHook(vm, 'beforeUpdate')
複製程式碼
注意,由於 Vue 的非同步更新機制,beforeUpdate
的呼叫已經是在 nextTick
中了。
具體程式碼如下:
nextTick(flushSchedulerQueue)
function flushSchedulerQueue {
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
// callHook(vm, 'beforeUpdate')
watcher.before()
}
}
}
複製程式碼
beforeUpdate被呼叫完成
然後經歷了一系列的 patch
、diff
流程後,元件重新渲染完畢,呼叫 updated
鉤子。
注意,這裡是對 watcher
倒序 updated
呼叫的。
也就是說,假如同一個屬性通過 props
分別流向 父 -> 子 -> 孫
這個路徑,那麼收集到依賴的先後也是這個順序,但是觸發 updated
鉤子確是 孫 -> 子 -> 父
這個順序去觸發的。
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
複製程式碼
updated被呼叫完成
至此,渲染更新流程完畢。
銷燬流程
在剛剛所說的更新後的 patch
過程中,如果發現有元件在下一輪渲染中消失了,比如 v-for
對應的陣列中少了一個資料。那麼就會呼叫 removeVnodes
進入元件的銷燬流程。
removeVnodes
會呼叫 vnode
的 destroy
生命週期,而 destroy
內部則會呼叫我們相對比較熟悉的 vm.$destroy()
。(keep-alive 包裹的子元件除外)
這時,就會呼叫 callHook(vm, 'beforeDestroy')
beforeDestroy被呼叫完成
之後就會經歷一系列的清理
邏輯,清除父子關係、watcher
關閉等邏輯。但是注意,$destroy
並不會把元件從檢視上移除,如果想要手動銷燬一個元件,則需要我們自己去完成這個邏輯。
然後,呼叫最後的 callHook(vm, 'destroyed')
destroyed被呼叫完成
總結
至此為止,Vue 的生命週期我們就完整的回顧了一遍。知道各個生命週期之間發生了什麼事,可以讓我們在編寫 Vue 元件的過程中更加胸有成竹。
希望這篇文章對你有幫助。
❤️感謝大家
1.如果本文對你有幫助,就點個贊支援下吧,你的「贊」是我創作的動力。
2.關注公眾號「前端從進階到入院」即可加我好友,我拉你進「前端進階交流群」,大家一起共同交流和進步。