基礎(chǔ)知識(shí)
Vue的生命周期
大自然有春夏秋冬,人有生老病死,優(yōu)秀的Vue當(dāng)然也存在自己的生命周期。
對(duì)于Vue來說它的生命周期就是Vue實(shí)例從創(chuàng)建到銷毀的過程。
生命周期函數(shù)
在生命周期的過程中運(yùn)行著一些叫做生命周期的函數(shù),給予了開發(fā)者在不同的生命周期階段添加業(yè)務(wù)代碼的能力。
在網(wǎng)上的一些文章中有的也叫它們生命周期鉤子,那鉤子又是什么呢?
鉤子函數(shù)
其實(shí)和回調(diào)是一個(gè)概念,當(dāng)系統(tǒng)執(zhí)行到某處時(shí),檢查是否有hook(鉤子),有的話就會(huì)執(zhí)行回調(diào)。
此hook非彼hook。
通俗的說,hook就是在程序運(yùn)行中,在某個(gè)特定的位置,框架的開發(fā)者設(shè)計(jì)好了一個(gè)鉤子來告訴我們當(dāng)前程序已經(jīng)運(yùn)行到特定的位置了,會(huì)觸發(fā)一個(gè)回調(diào)函數(shù),并提供給我們,讓我們可以在生命周期的特定階段進(jìn)行相關(guān)業(yè)務(wù)代碼的編寫。
我在官方提供的圖片上添加了相關(guān)注釋,希望能夠讓大家看的更明白一些,如下圖。

雖然添加了很多注釋,看不懂不要慌,我們來逐一進(jìn)行講解。
總的來說,Vue的生命周期可以分為以下八個(gè)階段:
beforeCreate 實(shí)例創(chuàng)建前
created 實(shí)例創(chuàng)建完成
beforeMount 掛載前
mounted 掛載完成
beforeUpdate 更新前
updated 更新完成
beforeDestory 銷毀前
destoryed 銷毀完成
1.beforeCreate
這個(gè)鉤子是new Vue()之后觸發(fā)的第一個(gè)鉤子,在當(dāng)前階段中data、methods、computed以及watch上的數(shù)據(jù)和方法均不能被訪問。
2.created
這個(gè)鉤子在實(shí)例創(chuàng)建完成后發(fā)生,當(dāng)前階段已經(jīng)完成了數(shù)據(jù)觀測(cè),也就是可以使用數(shù)據(jù),更改數(shù)據(jù),在這里更改數(shù)據(jù)不會(huì)觸發(fā)updated函數(shù)。可以做一些初始數(shù)據(jù)的獲取,在當(dāng)前階段無法與Dom進(jìn)行交互,如果你非要想,可以通過vm.$nextTick來訪問Dom。
3.beforeMounted
這個(gè)鉤子發(fā)生在掛載之前,在這之前template模板已導(dǎo)入渲染函數(shù)編譯。而當(dāng)前階段虛擬Dom已經(jīng)創(chuàng)建完成,即將開始渲染。在此時(shí)也可以對(duì)數(shù)據(jù)進(jìn)行更改,不會(huì)觸發(fā)updated。
4.mounted
這個(gè)鉤子在掛載完成后發(fā)生,在當(dāng)前階段,真實(shí)的Dom掛載完畢,數(shù)據(jù)完成雙向綁定,可以訪問到Dom節(jié)點(diǎn),使用$ref屬性對(duì)Dom進(jìn)行操作。也可以向后臺(tái)發(fā)送請(qǐng)求,拿到返回?cái)?shù)據(jù)。
5.beforeUpdate
這個(gè)鉤子發(fā)生在更新之前,也就是響應(yīng)式數(shù)據(jù)發(fā)生更新,虛擬dom重新渲染之前被觸發(fā),你可以在當(dāng)前階段進(jìn)行更改數(shù)據(jù),不會(huì)造成重渲染。
6.updated
這個(gè)鉤子發(fā)生在更新完成之后,當(dāng)前階段組件Dom已完成更新。要注意的是避免在此期間更改數(shù)據(jù),因?yàn)檫@可能會(huì)導(dǎo)致無限循環(huán)的更新。
7.beforeDestroy
這個(gè)鉤子發(fā)生在實(shí)例銷毀之前,在當(dāng)前階段實(shí)例完全可以被使用,我們可以在這時(shí)進(jìn)行善后收尾工作,比如清除計(jì)時(shí)器。
8.destroyed
這個(gè)鉤子發(fā)生在實(shí)例銷毀之后,這個(gè)時(shí)候只剩下了dom空殼。組件已被拆解,數(shù)據(jù)綁定被卸除,監(jiān)聽被移出,子實(shí)例也統(tǒng)統(tǒng)被銷毀。
注意點(diǎn)
在使用生命周期時(shí)有幾點(diǎn)注意事項(xiàng)需要我們牢記。
1.除了beforeCreate和created鉤子之外,其他鉤子均在服務(wù)器端渲染期間不被調(diào)用。
2.上文曾提到過,在updated的時(shí)候千萬不要去修改data里面賦值的數(shù)據(jù),否則會(huì)導(dǎo)致死循環(huán)。
3.Vue的所有生命周期函數(shù)都是自動(dòng)綁定到this的上下文上。所以,你這里使用箭頭函數(shù)的話,就會(huì)出現(xiàn)this指向的父級(jí)作用域,就會(huì)報(bào)錯(cuò)。原因下面源碼部分會(huì)講解。
源碼解讀
因?yàn)閂ue的源碼部分包含很多內(nèi)容,本文只選取生命周期相關(guān)的關(guān)鍵性代碼進(jìn)行解析。同時(shí)也強(qiáng)烈推薦大家學(xué)習(xí)Vue源碼的其他內(nèi)容,因?yàn)檫@個(gè)框架真的很優(yōu)秀,附上鏈接[Vue.js技術(shù)揭秘](https://ustbhuangyi.github.io/vue-analysis/prepare/)。
我們先來從源碼中來解答上文注意點(diǎn)的第四個(gè)問題(以下所有代碼都有刪減,用…代替刪減部分)。
// src/core/instance/lifecycle.js
// callhook 函數(shù)的功能就是在當(dāng)前vue組件實(shí)例中,調(diào)用某個(gè)生命周期鉤子注冊(cè)的所有回調(diào)函數(shù)。
// vm:Vue實(shí)例
// hook:生命周期名字
export function callHook (vm: Component, hook: string) {
pushTarget()
const handlers = vm.$options[hook]
// 初始化合并 options 的過程 、,將各個(gè)生命周期函數(shù)合并到 options 里
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()
}
// src/core/util/error.js
export function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
我們從上面的代碼中可以看到callHook中調(diào)用了invokeWithErrorHandling方法,在invokeWithErrorHandling方法中,使用了apply和call改變了this指向,而在箭頭函數(shù)中this指向是無法改變的,所以我們?cè)诰帉懮芷诤瘮?shù)的時(shí)候不能使用箭頭函數(shù)。
解答完上面遺留的問題后,我們?cè)賮碇鹨恢v解各個(gè)生命周期。
1.beforeCreate和created
// src/core/instance/init
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
// 合并選項(xiàng)部分已省略
initLifecycle(vm)
// 主要就是給vm對(duì)象添加了 $parent、$root、$children 屬性,以及一些其它的生命周期相關(guān)的標(biāo)識(shí)
initEvents(vm) // 初始化事件相關(guān)的屬性
initRender(vm) // vm 添加了一些虛擬 dom、slot 等相關(guān)的屬性和方法
callHook(vm, 'beforeCreate') // 調(diào)用 beforeCreate 鉤子
//下面 initInjections(vm) 和 initProvide(vm) 兩個(gè)配套使用,用于將父組件 _provided 中定義的值,通過 inject 注入到子組件,且這些屬性不會(huì)被觀察
initInjections(vm)
initState(vm) // props、methods、data、watch、computed等數(shù)據(jù)初始化
initProvide(vm)
callHook(vm, 'created') // 調(diào)用 created 鉤子
}
}
// src/core/instance/state
export 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 /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
我們可以看到beforeCreate鉤子調(diào)用是在initState之前的,而從上面的第二段代碼我們可以看出initState的作用是對(duì)props、methods、data、computed、watch等屬性做初始化處理。
通過閱讀源碼,我們更加清楚的明白了在beforeCreate鉤子的時(shí)候我們沒有對(duì)props、methods、data、computed、watch上的數(shù)據(jù)的訪問權(quán)限。在created中才可以。
2.beforeMount和mounted
// mountComponent 核心就是先實(shí)例化一個(gè)渲染W(wǎng)atcher
// 在它的回調(diào)函數(shù)中會(huì)調(diào)用 updateComponent 方法
// 兩個(gè)核心方法 vm._render(生成虛擬Dom) 和 vm._update(映射到真實(shí)Dom)
// src/core/instance/lifecycle
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
...
}
callHook(vm, 'beforeMount') // 調(diào)用 beforeMount 鉤子
let updateComponent
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
// 將虛擬 Dom 映射到真實(shí) Dom 的函數(shù)。
// vm._update 之前會(huì)先調(diào)用 vm._render() 函數(shù)渲染 VNode
...
const vnode = vm._render()
...
vm._update(vnode, hydrating)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
}
new Watcher(vm, updateComponent, noop, {
before () {
// 先判斷是否 mouted 完成 并且沒有被 destroyed
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted') //調(diào)用 mounted 鉤子
}
return vm
}
通過上面的代碼,我們可以看出在執(zhí)行vm._render()函數(shù)渲染VNode之前,執(zhí)行了 beforeMount鉤子函數(shù),在執(zhí)行完 vm._update()把VNode patch到真實(shí)Dom后,執(zhí)行 mouted鉤子。也就明白了為什么直到mounted階段才名正言順的拿到了Dom。
3.beforeUpdate和updated
// src/core/instance/lifecycle
new Watcher(vm, updateComponent, noop, {
before () {
// 先判斷是否 mouted 完成 并且沒有被 destroyed
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate') // 調(diào)用 beforeUpdate 鉤子
}
}
}, true /* isRenderWatcher */)
// src/core/observer/scheduler
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) {
// 只有滿足當(dāng)前 watcher 為 vm._watcher(也就是當(dāng)前的渲染watcher)
// 以及組件已經(jīng) mounted 并且沒有被 destroyed 才會(huì)執(zhí)行 updated 鉤子函數(shù)。
callHook(vm, 'updated') // 調(diào)用 updated 鉤子
}
}
}
第一段代碼就是在beforeMount和mounted鉤子中間出現(xiàn)的,那么watcher中究竟做了些什么呢?第二段代碼的callUpdatedHooks函數(shù)中什么時(shí)候才可以滿足條件并執(zhí)行updated呢?我們來接著往下看。
// src/instance/observer/watcher.js
export default class Watcher {
...
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
// 在它的構(gòu)造函數(shù)里會(huì)判斷 isRenderWatcher,
// 接著把當(dāng)前 watcher 的實(shí)例賦值給 vm._watcher
isRenderWatcher?: boolean
) {
// 還把當(dāng)前 wathcer 實(shí)例 push 到 vm._watchers 中,
// vm._watcher 是專門用來監(jiān)聽 vm 上數(shù)據(jù)變化然后重新渲染的,
// 所以它是一個(gè)渲染相關(guān)的 watcher,因此在 callUpdatedHooks 函數(shù)中,
// 只有 vm._watcher 的回調(diào)執(zhí)行完畢后,才會(huì)執(zhí)行 updated 鉤子函數(shù)
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
...
}
看到這里我們明白了Vue是通過watcher來監(jiān)聽實(shí)例上的數(shù)據(jù)變化,進(jìn)而控制渲染流程。
4.beforeDestroy和destroyed
// src/core/instance/lifecycle.js
// 在 $destroy 的執(zhí)行過程中,它會(huì)執(zhí)行 vm.__patch__(vm._vnode, null)
// 觸發(fā)它子組件的銷毀鉤子函數(shù),這樣一層層的遞歸調(diào)用,
// 所以 destroy 鉤子函數(shù)執(zhí)行順序是先子后父,和 mounted 過程一樣。
Vue.prototype.$destroy = function () {
const vm: Component = this
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy') // 調(diào)用 beforeDestroy 鉤子
vm._isBeingDestroyed = true
// 一些銷毀工作
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// 拆卸 watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
...
vm._isDestroyed = true
// 調(diào)用當(dāng)前 rendered tree 上的 destroy 鉤子
// 發(fā)現(xiàn)子組件,會(huì)先去銷毀子組件
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed') // 調(diào)用 destroyed 鉤子
// 關(guān)閉所有實(shí)例偵聽器。
vm.$off()
// 刪除 __vue__ 引用
if (vm.$el) {
vm.$el.__vue__ = null
}
// 釋放循環(huán)引用
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
通過上面的代碼,我們了解了組件銷毀階段的拆卸過程,其中會(huì)執(zhí)行一個(gè)__patch__函數(shù),講解起來篇幅較多,想要深入了解該部分的同學(xué)可以自行閱讀源碼解讀處給大家的鏈接。
除了這八種鉤子外,我們?cè)诠倬W(wǎng)也可以查閱到另外幾種不常用的鉤子,這里列舉出來。
幾種不常用的鉤子
activated
keep-alive 組件激活時(shí)調(diào)用,該鉤子在服務(wù)器端渲染期間不被調(diào)用。
deactivated
keep-alive 組件停用時(shí)調(diào)用,該鉤子在服務(wù)器端渲染期間不被調(diào)用。
errorCaptured
當(dāng)捕獲一個(gè)來自子孫組件的錯(cuò)誤時(shí)被調(diào)用。此鉤子會(huì)收到三個(gè)參數(shù):錯(cuò)誤對(duì)象、發(fā)生錯(cuò)誤的組件實(shí)例以及一個(gè)包含錯(cuò)誤來源信息的字符串。此鉤子可以返回 false 以阻止該錯(cuò)誤繼續(xù)向上傳播
你可以在此鉤子中修改組件的狀態(tài)。因此在模板或渲染函數(shù)中設(shè)置其它內(nèi)容的短路條件非常重要,它可以防止當(dāng)一個(gè)錯(cuò)誤被捕獲時(shí)該組件進(jìn)入一個(gè)無限的渲染循環(huán)。