相信你們對 Vue 有哪些生命週期早就已經爛熟於心,可是對於這些生命週期的先後分別作了哪些事情,可能還有些不熟悉。html
本篇文章就從一個完整的流程開始,詳細講解各個生命週期之間發生了什麼事情。前端
注意本文不涉及 keep-alive
的場景和錯誤處理的場景。vue
從 new Vue(options)
開始做爲入口,Vue
只是一個簡單的構造函數,內部是這樣的:node
function Vue (options) {
this._init(options)
}
複製代碼
進入了 _init
函數以後,先初始化了一些屬性。react
initLifecycle
:初始化一些屬性如$parent
,$children
。根實例沒有 $parent
,$children
開始是空數組,直到它的 子組件
實例進入到 initLifecycle
時,纔會往父組件的 $children
裏把自身放進去。因此 $children
裏的必定是組件的實例。initEvents
:初始化事件相關的屬性,如 _events
等。initRender
:初始化渲染相關如 $createElement
,而且定義了 $attrs
和 $listeners
爲淺層
響應式屬性。具體能夠查看細節
章節。而且還定義了$slots
、$scopedSlots
,其中 $slots
是馬上賦值的,可是 $scopedSlots
初始化的時候是一個 emptyObject
,直到組件的 vm._render
過程當中纔會經過 normalizeScopedSlots
去把真正的 $scopedSlots
整合後掛到 vm
上。而後開始第一個生命週期:算法
callHook(vm, 'beforeCreate')
複製代碼
beforeCreate
以後vue-cli
inject
state
props
methods
data
computed
watch
provide
因此在 data
中可使用 props
上的值,反過來則不行。數組
而後進入 created
階段:app
callHook(vm, 'created')
複製代碼
調用 $mount
方法,開始掛載組件到 dom
上。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
函數後,咱們就能夠往渲染
的步驟繼續進行了
把 渲染組件的函數
定義好,具體代碼是:
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
也是有本身的 生命週期的
,只不過咱們日常開發的時候是接觸不到的。
那麼子組件的 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
生命週期被觸發。
到此爲止,組件的掛載就完成了,初始化的生命週期結束。
當一個響應式屬性被更新後,觸發了 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()
}
}
}
複製代碼
而後經歷了一系列的 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')
}
}
}
複製代碼
至此,渲染更新流程完畢。
在剛剛所說的更新後的 patch
過程當中,若是發現有組件在下一輪渲染中消失了,好比 v-for
對應的數組中少了一個數據。那麼就會調用 removeVnodes
進入組件的銷燬流程。
removeVnodes
會調用 vnode
的 destroy
生命週期,而 destroy
內部則會調用咱們相對比較熟悉的 vm.$destroy()
。(keep-alive 包裹的子組件除外)
這時,就會調用 callHook(vm, 'beforeDestroy')
以後就會經歷一系列的清理
邏輯,清除父子關係、watcher
關閉等邏輯。可是注意,$destroy
並不會把組件從視圖上移除,若是想要手動銷燬一個組件,則須要咱們本身去完成這個邏輯。
而後,調用最後的 callHook(vm, 'destroyed')
這裏額外提一下 $attrs
之因此只有第一層被定義爲響應式,是由於通常來講深層次的響應式定義已經在父組件中定義作好了,只要保證 vm.$attrs = newAttrs
這樣的操做能觸發子組件的響應式更新便可。(在子組件的模板中使用了 $attrs
的狀況下)
在更新子組件 updateChildComponent
操做中,會去取收集到的 vnode
上的 attrs
和 listeners
去更新 $attrs
屬性,這樣就算子組件的模板上用了 $attrs
的屬性也可觸發響應式的更新。
import { emptyObject } from '../util/index'
vm.$attrs = parentVnode.data.attrs || emptyObject
vm.$listeners = listeners || emptyObject
複製代碼
有一個比較細節的操做是這樣的:
這裏的 emptyObject
永遠是一樣的引用,也就能保證在沒有 attrs
或 listeners
傳遞的時候,可以永遠用同一個引用而不去觸發響應式更新。
由於 defineReactive
的 set
函數中會作這樣的判斷:
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
// 這裏引用相等 直接返回了
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
}
複製代碼
上文中提到,子組件的初始化也同樣會走 _init
方法,可是和根 Vue
實例不一樣的是,在 _init
中會有一個分支邏輯。
if (options && options._isComponent) {
// 若是是組件的話 走這個邏輯
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
複製代碼
根級別 Vue 實例,也就是 new Vue(options)
生成是實例,它的 $options
對象大概是這種格式的,咱們定義在 new Vue(options)
中的 options
對象直接合併到了 $options
上。
beforeCreate: [ƒ]
beforeMount: [ƒ]
components: {test: {…}}
created: [ƒ]
data: ƒ mergedInstanceDataFn()
directives: {}
el: "#app"
filters: {}
methods: {change: ƒ}
mixins: [{…}]
mounted: [ƒ]
name: "App"
render: ƒ anonymous( )
複製代碼
而子組件實例上的 $options
則是這樣的:
parent: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
propsData: {msg: "hello"}
render: ƒ anonymous( )
staticRenderFns: []
_componentTag: "test"
_parentListeners: undefined
_parentVnode: VNode {tag: "vue-component-1-test", data: {…}, children: undefined, text: undefined, elm: li, …}
_propKeys: ["msg"]
_renderChildren: [VNode]
__proto__: Object
複製代碼
那有人會問了,爲啥我在子組件裏經過 this.$options
也能訪問到定義在 options
裏的屬性啊?
咱們展開 __proto__
屬性看一下:
beforeCreate: [ƒ]
beforeMount: [ƒ]
created: [ƒ]
directives: {}
filters: {}
mixins: [{…}]
mounted: [ƒ]
props: {msg: {…}}
_Ctor: {0: ƒ}
_base: ƒ Vue(options)
複製代碼
原來是被掛在原型上了,具體是 initInternalComponent
中的這段話作的:
const opts = vm.$options = Object.create(vm.constructor.options)
複製代碼
實例上有兩個屬性老是讓人摸不着頭腦,就是 $vnode
和 _vnode
,
舉個例子來講,咱們寫了個這樣的組件 App
:
<div class="class-app">
<test />
</div>
複製代碼
test
組件
<li class="class-test">
Hi, I'm test
</li>
複製代碼
接下來咱們都以 test
組件舉例,請仔細看清楚它們的父子關係以及使用的標籤和類名。
在渲染 App
組件的時候,遇到了 test
標籤,會把 test
組件包裹成一個 vnode
:
<div class="class-app">
// 渲染到這裏
<test />
</div>
複製代碼
形如此:
tag: "vue-component-1-test"
elm: li.class-test
componentInstance: VueComponent {_uid: 1, _isVue: true, $options: {…},
componentOptions: {propsData: {…}, listeners: undefined, tag: "test", children: Array(1), Ctor: ƒ}
context: Vue {_uid: 0, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: Vue, …}
data: {attrs: {…}, on: undefined, hook: {…}, pendingInsert: null}
child: (...)
複製代碼
這個 tag
爲 vue-component-1-test
的 vnode
,其實能夠說是把整個組件給包裝了起來,經過 componentInstance
屬性能夠訪問到實例 this
,
在 test
組件(好比說 test.vue
文件)的視角來看,它應該算是 外部 的 vnode
。(父組件在模板中讀取到 test.vue
組件後才生成)
它的 elm
屬性指向組件內部的 根元素
,也就是 li.class-test
。
此時,它在 test
組件的實例 this
上就保存爲 this.$vnode
。
在 test
組件實例上,經過 this._vnode
訪問到的 vnode
形如這樣:
tag: "li"
elm: li.class-test
children: (2) [VNode, VNode]
context: VueComponent {_uid: 1, _isVue: true, $options: {…}, _renderProxy: Proxy, _self: VueComponent, …}
data: {staticClass: "class-test"}
parent: VNode {tag: "vue-component-1-test", data: {…}, children: undefined, text: undefined, elm: li.test, …}
複製代碼
能夠看到,它的 tag
是 li
,也就是 test
組件的 template
上聲明的 最外層的節點
,
它的 elm
屬性也指向組件內部的 根元素
,也就是 li.class-test
。
它其實就是 test
組件的 render
函數返回的 vnode
,
在 _update
方法中也找到了來源:
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
vm._vnode = vnode
}
複製代碼
回憶一下組件是怎麼初始化掛載和更新的,是否是 vm._update(vm._render())
?
所謂的 diff
算法,diff
的其實就是 this
上保存的_vnode
,和新調用 _render
去生成的 vnode
進行 patch
。
而根 Vue
實例,也就是 new Vue()
的那層實例, this.$vnode
就是 null
,由於並無外層組件去渲染它。
$vnode
外層組件渲染到當前組件標籤時,生成的 vnode
實例。
_vnode
是組件內部調用 render
函數返回的 vnode
實例。
_vnode.parent === $vnode
他們的 elm
,也就是實際 dom元素
,都指向組件內部的根元素
。
$children
只保存當前實例的直接子組件 實例,因此你訪問不到 button
,li
這些 原生html標籤
。注意是實例而不是 vnode
,也就是經過 this
訪問到的那玩意。
_vnode.children
,則會把當前組件的 vnode
樹所有保存起來,不論是組件vnode
仍是原生 html 標籤生成的vnode
,而且 原生 html生成的 vnode
內部還能夠經過children
進一步訪問子vnode
。
至此爲止,Vue 的生命週期咱們就完整的回顧了一遍。知道各個生命週期之間發生了什麼事,可讓咱們在編寫 Vue 組件的過程當中更加成竹在胸。
但願這篇文章對你有幫助。
1.若是本文對你有幫助,就點個贊支持下吧,你的「贊」是我創做的動力。
2.關注公衆號「前端從進階到入院」便可加我好友,我拉你進「前端進階交流羣」,你們一塊兒共同交流和進步。