#科學燃計劃#
在我們的實際專案中,與Vue的生命週期打交道可以說是家常便飯。掌握Vue的生命週期對開發者來說是特別重要的。那麼如果能夠從原始碼角度理解Vue的生命週期,對我們的開發和成長會有進一步的提升。
本文從基礎知識開始講起,分為基礎知識和原始碼解讀兩部分,對基礎知識已經掌握的開發者可自行跳躍。
基礎知識Vue的生命週期大自然有春夏秋冬,人有生老病死,優秀的Vue當然也存在自己的生命週期。
對於Vue來說它的生命週期就是Vue例項從建立到銷燬的過程。
生命週期函式在生命週期的過程中執行著一些叫做生命週期的函式,給予了開發者在不同的生命週期階段新增業務程式碼的能力。
在網上的一些文章中有的也叫它們生命週期鉤子,那鉤子又是什麼呢?
鉤子函式其實和回撥是一個概念,當系統執行到某處時,檢查是否有hook(鉤子),有的話就會執行回撥。
此hook非彼hook。
通俗的說,hook就是在程式執行中,在某個特定的位置,框架的開發者設計好了一個鉤子來告訴我們當前程式已經執行到特定的位置了,會觸發一個回撥函式,並提供給我們,讓我們可以在生命週期的特定階段進行相關業務程式碼的編寫。
雖然添加了很多註釋,看不懂不要慌,我們來逐一進行講解。
總的來說,Vue的生命週期可以分為以下八個階段:
“
beforeCreate 例項建立前
created 例項建立完成
beforeMount 掛載前
mounted 掛載完成
beforeUpdate 更新前
updated 更新完成
beforeDestory 銷燬前
destoryed 銷燬完成
”
1.beforeCreate這個鉤子是new Vue()之後觸發的第一個鉤子,在當前階段中data、methods、computed以及watch上的資料和方法均不能被訪問。
2.created這個鉤子在例項建立完成後發生,當前階段已經完成了資料觀測,也就是可以使用資料,更改資料,在這裡更改資料不會觸發updated函式。可以做一些初始資料的獲取,在當前階段無法與Dom進行互動,如果你非要想,可以透過vm.$nextTick來訪問Dom。
3.beforeMount這個鉤子發生在掛載之前,在這之前template模板已匯入渲染函式編譯。而當前階段虛擬Dom已經建立完成,即將開始渲染。在此時也可以對資料進行更改,不會觸發updated。
4.mounted這個鉤子在掛載完成後發生,在當前階段,真實的Dom掛載完畢,資料完成雙向繫結,可以訪問到Dom節點,使用$refs屬性對Dom進行操作。也可以向後臺傳送請求,拿到返回資料。
5.beforeUpdate這個鉤子發生在更新之前,也就是響應式資料發生更新,虛擬dom重新渲染之前被觸發,你可以在當前階段進行更改資料,不會造成重渲染。
6.updated這個鉤子發生在更新完成之後,當前階段元件Dom已完成更新。要注意的是避免在此期間更改資料,因為這可能會導致無限迴圈的更新。
7.beforeDestroy這個鉤子發生在例項銷燬之前,在當前階段例項完全可以被使用,我們可以在這時進行善後收尾工作,比如清除計時器。
8.destroyed這個鉤子發生在例項銷燬之後,這個時候只剩下了dom空殼。元件已被拆解,資料繫結被卸除,監聽被移出,子例項也統統被銷燬。
注意點在使用生命週期時有幾點注意事項需要我們牢記。
1.除了beforeCreate和created鉤子之外,其他鉤子均在伺服器端渲染期間不被呼叫。
2.上文曾提到過,在updated的時候千萬不要去修改data裡面賦值的資料,否則會導致死迴圈。
3.Vue的所有生命週期函式都是自動繫結到this的上下文上。所以,你這裡使用箭頭函式的話,就會出現this指向的父級作用域,就會報錯。原因下面原始碼部分會講解。
原始碼解讀因為Vue的原始碼部分包含很多內容,本文只選取生命週期相關的關鍵性程式碼進行解析。同時也強烈推薦大家學習Vue原始碼的其他內容,因為這個框架真的很優秀,附上鍊接Vue.js技術揭秘。
我們先來從原始碼中來解答上文注意點的第四個問題(以下所有程式碼都有刪減,用...代替刪減部分)。
// src/core/instance/lifecycle.js// callhook 函式的功能就是在當前vue元件例項中,呼叫某個生命週期鉤子註冊的所有回撥函式。// vm:Vue例項// hook:生命週期名字export function callHook (vm: Component, hook: string) { pushTarget() const handlers = vm.$options[hook] // 初始化合並 options 的過程 、,將各個生命週期函式合併到 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.jsexport 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中呼叫了invokeWithErrorHandling方法,在invokeWithErrorHandling方法中,使用了apply和call改變了this指向,而在箭頭函式中this指向是無法改變的,所以我們在編寫生命週期函式的時候不能使用箭頭函式。關於this指向問題請移步我的另一篇文章JavaScript This詳解。
解答完上面遺留的問題後,我們再來逐一講解各個生命週期。
1.beforeCreate和created// src/core/instance/initexport function initMixin (Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this ... // 合併選項部分已省略 initLifecycle(vm) // 主要就是給vm物件添加了 $parent、$root、$children 屬性,以及一些其它的生命週期相關的標識 initEvents(vm) // 初始化事件相關的屬性 initRender(vm) // vm 添加了一些虛擬 dom、slot 等相關的屬性和方法 callHook(vm, 'beforeCreate') // 呼叫 beforeCreate 鉤子 //下面 initInjections(vm) 和 initProvide(vm) 兩個配套使用,用於將父元件 _provided 中定義的值,透過 inject 注入到子元件,且這些屬性不會被觀察 initInjections(vm) initState(vm) // props、methods、data、watch、computed等資料初始化 initProvide(vm) callHook(vm, 'created') // 呼叫 created 鉤子 }}// src/core/instance/stateexport 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鉤子呼叫是在initState之前的,而從上面的第二段程式碼我們可以看出initState的作用是對props、methods、data、computed、watch等屬性做初始化處理。
透過閱讀原始碼,我們更加清楚的明白了在beforeCreate鉤子的時候我們沒有對props、methods、data、computed、watch上的資料的訪問許可權。在created中才可以。
2.beforeMount和mounted// mountComponent 核心就是先例項化一個渲染Watcher// 在它的回撥函式中會呼叫 updateComponent 方法// 兩個核心方法 vm._render(生成虛擬Dom) 和 vm._update(對映到真實Dom)// src/core/instance/lifecycleexport function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean): Component { vm.$el = el if (!vm.$options.render) { vm.$options.render = createEmptyVNode ... } callHook(vm, 'beforeMount') // 呼叫 beforeMount 鉤子 let updateComponent if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { // 將虛擬 Dom 對映到真實 Dom 的函式。 // vm._update 之前會先呼叫 vm._render() 函式渲染 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') //呼叫 mounted 鉤子 } return vm}
透過上面的程式碼,我們可以看出在執行vm._render()函式渲染VNode之前,執行了 beforeMount鉤子函式,在執行完 vm._update()把VNode patch到真實Dom後,執行 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') // 呼叫 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) { // 只有滿足當前 watcher 為 vm._watcher(也就是當前的渲染watcher) // 以及元件已經 mounted 並且沒有被 destroyed 才會執行 updated 鉤子函式。 callHook(vm, 'updated') // 呼叫 updated 鉤子 } } }
第一段程式碼就是在beforeMount和mounted鉤子中間出現的,那麼watcher中究竟做了些什麼呢?第二段程式碼的callUpdatedHooks函式中什麼時候才可以滿足條件並執行updated呢?我們來接著往下看。
// src/instance/observer/watcher.jsexport default class Watcher { ... constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, // 在它的構造函數里會判斷 isRenderWatcher, // 接著把當前 watcher 的例項賦值給 vm._watcher isRenderWatcher?: boolean ) { // 還把當前 wathcer 例項 push 到 vm._watchers 中, // vm._watcher 是專門用來監聽 vm 上資料變化然後重新渲染的, // 所以它是一個渲染相關的 watcher,因此在 callUpdatedHooks 函式中, // 只有 vm._watcher 的回撥執行完畢後,才會執行 updated 鉤子函式 this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) ...}
看到這裡我們明白了Vue是透過watcher來監聽例項上的資料變化,進而控制渲染流程。
4.beforeDestroy和destroyed // src/core/instance/lifecycle.js // 在 $destroy 的執行過程中,它會執行 vm.__patch__(vm._vnode, null) // 觸發它子元件的銷燬鉤子函式,這樣一層層的遞迴呼叫, // 所以 destroy 鉤子函式執行順序是先子後父,和 mounted 過程一樣。 Vue.prototype.$destroy = function () { const vm: Component = this if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy') // 呼叫 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 // 呼叫當前 rendered tree 上的 destroy 鉤子 // 發現子元件,會先去銷燬子元件 vm.__patch__(vm._vnode, null) callHook(vm, 'destroyed') // 呼叫 destroyed 鉤子 // 關閉所有例項偵聽器。 vm.$off() // 刪除 __vue__ 引用 if (vm.$el) { vm.$el.__vue__ = null } // 釋放迴圈引用 if (vm.$vnode) { vm.$vnode.parent = null } }}
透過上面的程式碼,我們瞭解了元件銷燬階段的拆卸過程,其中會執行一個__patch__函式,講解起來篇幅較多,想要深入瞭解該部分的同學可以自行閱讀原始碼解讀處給大家的連結。
除了這八種鉤子外,我們在官網也可以查閱到另外幾種不常用的鉤子,這裡列舉出來。
幾種不常用的鉤子activatedkeep-alive 元件啟用時呼叫,該鉤子在伺服器端渲染期間不被呼叫。
deactivatedkeep-alive 元件停用時呼叫,該鉤子在伺服器端渲染期間不被呼叫。
你可以在此鉤子中修改元件的狀態。因此在模板或渲染函式中設定其它內容的短路條件非常重要,它可以防止當一個錯誤被捕獲時該元件進入一個無限的渲染迴圈。