觀感度:??????????
口味:蜜桃烏龍
烹飪時間:30min
在我們的實際項目中,與Vue的生命周期打交道可以說是家常便飯。掌握Vue的生命周期對開發者來說是特別重要的。那麼如果能夠從源碼角度理解Vue的生命周期,對我們的開發和成長會有進一步的提升。
本文從基礎知識開始講起,分為基礎知識和源碼解讀兩部分,對基礎知識已經掌握的開發者可自行跳躍。
Vue的生命周期
大自然有春夏秋冬,人有生老病死,優秀的Vue當然也存在自己的生命周期。
對於Vue來說它的生命周期就是Vue實例從創建到銷毀的過程。
生命周期函數
在生命周期的過程中運行著一些叫做生命周期的函數,給予了開發者在不同的生命周期階段添加業務代碼的能力。
在網上的一些文章中有的也叫它們生命周期鉤子,那鉤子又是什麼呢?
鉤子函數
其實和回調是一個概念,當系統執行到某處時,檢查是否有hook(鉤子),有的話就會執行回調。
此hook非彼hook。
通俗的說,hook就是在程序運行中,在某個特定的位置,框架的開發者設計好了一個鉤子來告訴我們當前程序已經運行到特定的位置了,會觸發一個回調函數,並提供給我們,讓我們可以在生命周期的特定階段進行相關業務代碼的編寫。
我在官方提供的圖片上添加了相關注釋,希望能夠讓大家看的更明白一些,如下圖。
雖然添加了很多注釋,看不懂不要慌,我們來逐一進行講解。
總的來說,Vue的生命周期可以分為以下八個階段:
這個鉤子是new Vue()之後觸發的第一個鉤子,在當前階段中data、methods、computed以及watch上的數據和方法均不能被訪問。
這個鉤子在實例創建完成後發生,當前階段已經完成了數據觀測,也就是可以使用數據,更改數據,在這裡更改數據不會觸發updated函數。可以做一些初始數據的獲取,注意請求數據不易過多,會造成白屏時間過長。在當前階段無法與Dom進行交互,如果你非要想,可以通過vm.$nextTick來訪問Dom。
這個鉤子發生在掛載之前,在這之前template模板已導入渲染函數編譯。而當前階段虛擬Dom已經創建完成,即將開始渲染。在此時也可以對數據進行更改,不會觸發updated。
這個鉤子在掛載完成後發生,在當前階段,真實的Dom掛載完畢,數據完成雙向綁定,可以訪問到Dom節點,使用`$ref`屬性對Dom進行操作。也可以向後台發送請求,拿到返回數據。
這個鉤子發生在更新之前,也就是響應式數據發生更新,虛擬dom重新渲染之前被觸發,你可以在當前階段進行更改數據,不會造成重渲染。
這個鉤子發生在更新完成之後,當前階段組件Dom已完成更新。要注意的是避免在此期間更改數據,因為這可能會導致無限循環的更新。
這個鉤子發生在實例銷毀之前,在當前階段實例完全可以被使用,我們可以在這時進行善後收尾工作,比如清除計時器。
這個鉤子發生在實例銷毀之後,這個時候只剩下了dom空殼。組件已被拆解,數據綁定被卸除,監聽被移出,子實例也統統被銷毀。
注意點
在使用生命周期時有幾點注意事項需要我們牢記。
1.第一點就是上文曾提到的created階段的ajax請求與mounted請求的區別:前者頁面視圖未出現,如果請求信息過多,頁面會長時間處於白屏狀態。
2.除了beforeCreate和created鉤子之外,其他鉤子均在伺服器端渲染期間不被調用。
3.上文曾提到過,在updated的時候千萬不要去修改data裡面賦值的數據,否則會導致死循環。
4.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.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中調用了invokeWithErrorHandling方法,在invokeWithErrorHandling方法中,使用了apply和call改變了this指向,而在箭頭函數中this指向是無法改變的,所以我們在編寫生命周期函數的時候不能使用箭頭函數。關於this指向問題請移步我的另一篇文章治療this「皮」的詳細藥方
解答完上面遺留的問題後,我們再來逐一講解各個生命周期。
// src/core/instance/init export 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/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鉤子調用是在initState之前的,而從上面的第二段代碼我們可以看出initState的作用是對props、methods、data、computed、watch等屬性做初始化處理。
通過閱讀源碼,我們更加清楚的明白了在beforeCreate鉤子的時候我們沒有對props、methods、data、computed、watch上的數據的訪問許可權。在created中才可以。
// mountComponent 核心就是先實例化一個渲染Watcher // 在它的回調函數中會調用 updateComponent 方法 // 兩個核心方法 vm._render(生成虛擬Dom) 和 vm._update(映射到真實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) // 調用 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。
// 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呢?我們來接著往下看。
export 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來監聽實例上的數據變化,進而控制渲染流程。
// 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__函數,講解起來篇幅較多,想要深入了解該部分的同學可以自行閱讀源碼解讀處給大家的鏈接。
除了這八種鉤子外,我們在官網也可以查閱到另外幾種不常用的鉤子,這裡列舉出來。
幾種不常用的鉤子
activated
keep-alive 組件激活時調用,該鉤子在伺服器端渲染期間不被調用。
deactivated
keep-alive 組件停用時調用,該鉤子在伺服器端渲染期間不被調用。
errorCaptured
當捕獲一個來自子孫組件的錯誤時被調用。此鉤子會收到三個參數:錯誤對象、發生錯誤的組件實例以及一個包含錯誤來源信息的字元串。此鉤子可以返回 false 以阻止該錯誤繼續向上傳播
你可以在此鉤子中修改組件的狀態。因此在模板或渲染函數中設置其它內容的短路條件非常重要,它可以防止當一個錯誤被捕獲時該組件進入一個無限的渲染循環。
感謝大家的閱讀,如果存在錯誤的地方希望能夠指出以便及時改正。
微信公眾號會同步發送優質推文。
期待您的關注!
推薦閱讀: