最近在工做之餘也陸陸續續研究了vue2.x的源碼,打算經過文章來記錄對源碼的一些學習收穫和總結,本人第一次寫文章,有點緊張又有點激動,但願從此能繼續堅持下去。這篇是經過源碼來分析整個生命週期執行的機制,若是文章有錯誤不對的地方,歡迎指出,不勝感謝,若是對你有幫助,請爲我點個贊吧,謝謝。javascript
//子組件 var sub = { template: '<div class="sub"></div>' } //父組件 new Vue({ components: {sub}, template: `<div class="parent"> <sub></sub> <p></p> </div>` }) 子組件實例vm{ $vnode 指的是父vnode,即例子裏父組件裏<sub></sub>這個vnode _vnode 指的是渲染vnode,即自己渲染的元素,即例子裏的<div class="sub"></div> }
咱們從入口分析起,new Vue的時候,會執行原型裏的_init的初始化方法。vue
function Vue (options) { ... this._init(options); } Vue.prototype._init = function (options) { var vm = this; initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); initInjections(vm); initState(vm); initProvide(vm); callHook(vm, 'created'); ... };
咱們來看一下他的_init方法,這裏簡化了一下代碼,去掉了跟生命週期無關的,咱們看到會在執行了initLifecycle(vm);initEvents(vm);initRender(vm);而後執行了callHook(vm, 'beforeCreate')的方法,這裏就觸發了vue實例上beforeCreate鉤子的執行,我麼來看一下callHook的實現,以後全部生命週期的執行,都會經過這個函數傳入不一樣生命週期參數來實現。java
function callHook (vm, hook) { pushTarget(); var handlers = vm.$options[hook]; var info = hook + " hook"; if (handlers) { for (var i = 0, j = handlers.length; i < j; i++) { invokeWithErrorHandling(handlers[i], vm, null, vm, info); } } if (vm._hasHookEvent) { vm.$emit('hook:' + hook); } popTarget(); }
這裏咱們能夠看到他拿到$options定義的生命週期函數數組,進行遍歷執行,組件上顯性去定義的node
vm.$on('hook:xxx',()=>{})
自定義事件也會在這裏進行調用執行。
以後就是執行了initInjections(vm); initState(vm);initProvide(vm);這裏是對組件上inject,data,props,watches,computed等屬性進行響應式綁定後,執行了created的生命週期鉤子。
#### 父子組件執行順序
咱們知道,父組件在patch過程當中,當遇到組件vnode,組件vnode會執行createComponent方法,而後進行子組件構造函數的實例化,也會執行vue初始化的一整套流程,由於父是先比子建立的,因此執行順序會是算法
父beforeCreate > 父created > 子beforeCreate > 子created數組
## beforeMount和mounted鉤子
組件在進行cteated以後,要執行$mount(mountComponent)方法,而後執行裏面的render和patch方法,進行組件的掛載。緩存
function mountComponent ( vm, el, hydrating ) { ... vm.$el = el; callHook(vm, 'beforeMount'); var updateComponent; updateComponent = function () { vm._update(vm._render(), hydrating); }; ... if (vm.$vnode == null) { vm._isMounted = true; callHook(vm, 'mounted'); } return vm }
這裏patch以前會執行beforeMount鉤子,而這個函數裏要執行mounted鉤子,是要在vm.$vnode爲null的狀況下,$vnode咱們知道是組件的父組件vnode。可是子組件咱們知道都是有$vnode的,那麼他會在哪裏去觸發mounted鉤子呢,其實vue的根實例經過createElm建立真實dom時插入文檔時,會傳入insertedVnodeQueue,在遞歸過程當中去收集子組件實例,而後最後在整個真實dom插入文檔後,經過invokeInsertHook來遍歷執行子組件的mounted鉤子。最後根實例的\$vnode爲null,因此最後才進行mounted。dom
function patch (oldVnode, vnode, hydrating, removeOnly) { let insertedVnodeQueue = [] let isInitialPatch = false if (子組件初次建立時) { isInitialPatch = true ...} else { createElm( vnode, insertedVnodeQueue,//根實例建立真實dom時,會傳入insertedVnodeQueue,收集子組件的實例 oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ); } ... invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch); return vnode.elm } function invokeInsertHook (vnode, queue, initial) { <!--vnode是渲染vnode,它的parent是它父組件vnode--> if (isTrue(initial) && isDef(vnode.parent)) { //由於子組件生成真實Dom後,都會走到這裏,當判斷爲組件爲初次渲染且有父vnode //就不進行遍歷queue,而是把隊列裏保留在data.pendingInsert屬性裏,供後續父實例拿到當前隊列 //只有根實例的時候纔會執行遍歷insert鉤子,即觸發全部子組件的mounted鉤子。 vnode.parent.data.pendingInsert = queue; } else { for (var i = 0; i < queue.length; ++i) { queue[i].data.hook.insert(queue[i]); } } } 這裏介紹一下組件vnode建立過程當中會安裝一些組件鉤子,用於不一樣時候的調用,這裏的data.hook.insert就是組件的真實dom插入時會執行的鉤子 var componentVNodeHooks = { init: function init (vnode, hydrating) { ... }, prepatch: function prepatch (oldVnode, vnode) { ... }, <!--插入勾子--> insert: function insert (vnode) { var context = vnode.context; var componentInstance = vnode.componentInstance; if (!componentInstance._isMounted) { componentInstance._isMounted = true; callHook(componentInstance, 'mounted'); } <!--keep-alive時候調用--> if (vnode.data.keepAlive) { if (context._isMounted) { queueActivatedComponent(componentInstance); } else { activateChildComponent(componentInstance, true /* direct */); } } }, <!--銷燬勾子--> destroy: function destroy (vnode) { var componentInstance = vnode.componentInstance; if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy(); } else { <!--keep-alive時候調用--> deactivateChildComponent(componentInstance, true /* direct */); } } } };
這裏在父子組件嵌套時,會深度遍歷執行patch函數,子組件真實Dom會優先插入到父元素裏,因此子組件實例會先插入到insertedVnodeQueue。ide
由於patch函數是父先以子執行的,因此beforeMount是父>子,而子組件是優先插入到insertedVnodeQueue隊列裏,最後在遍歷過程,子組件的mmouted會先執行,因此mounted子>父,因此順序是函數
父beforeMount > 子beforMount > 子mounted > 父mounted
總體初次渲染的順序是
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
這兩個鉤子都是在組件更新的時候觸發的,在$mount(mountComponent)掛載的時候,還有這樣一段代碼
function mountComponent() { new Watcher(vm, updateComponent, noop, { before: function before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate'); } } }, true /* isRenderWatcher */); }
這裏是建立組件的渲染watcher,並傳入before函數,裏面是beforeUpdate鉤子的執行。咱們知道,當父子組件更新的時候,會根據響應式系統,調用watcher的方法update將watcher push到一個隊列裏,並會在下一個tick裏執行函數flushSchedulerQueue 遍歷queue進行更新,執行before函數觸發beforeCreate鉤子,並經過watcher.vm拿到組件實例,觸發updated勾子。
Watcher.prototype.update = function update () { ... queueWatcher(this); }; function queueWatcher (watcher) { ... nextTick(flushSchedulerQueue); } function flushSchedulerQueue () { ... for (index = 0; index < queue.length; index++) { watcher = queue[index]; if (watcher.before) { watcher.before(); } ... } ... var updatedQueue = queue.slice(); callUpdatedHooks(updatedQueue); } function callUpdatedHooks (queue) { var i = queue.length; while (i--) { var watcher = queue[i]; var vm = watcher.vm; if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { callHook(vm, 'updated'); } } }
由於queue隊列裏排列順序是父先以子,因此執行before函數時,是父beforeUpdate > 子beforeUpdate,而在callUpdatedHooks時,while循環時,是以最後的watcher遞減下來執行callHook(vm, 'updated'),因此總的執行順序是
父beforeUpdate > 子beforeUpdate > 子updated > 父updated
這兩個鉤子都是在組件銷燬過程當中執行的,在組件更新過程當中,會進行新舊vnode的diff算法,邏輯在patchVnode中的updateChildren函數裏,具體的邏輯你們能夠去源碼看看,由於比對中,就會去刪除一些沒用的節點,就會觸發removeVnodes函數,進而會執行invokeDestroyHook函數,去執行組件vnode裏的鉤子data.hook.destroy(可看一下上面代碼安裝在組件vnode的勾子有哪些)
function removeVnodes (parentElm, vnodes, startIdx, endIdx) { for (; startIdx <= endIdx; ++startIdx) { var ch = vnodes[startIdx]; if (isDef(ch)) { if (isDef(ch.tag)) { removeAndInvokeRemoveHook(ch); invokeDestroyHook(ch); } else { // Text node removeNode(ch.elm); } } } } function invokeDestroyHook (vnode) { var i, j; var data = vnode.data; if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); } for (i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](vnode); } } if (isDef(i = vnode.children)) { for (j = 0; j < vnode.children.length; ++j) { invokeDestroyHook(vnode.children[j]); } } }
在組件vnode的destroy鉤子裏,會執行componentInstance.$destroy();進而執行到下面Vue原型上掛載的\$destroy方法,vm.__patch__(vm._vnode, null)這個代碼會傳入vm_vnode和null,vm_vnode即渲染vnode,將其子vnode進行遞歸執行invokeDestroyHook方法進行銷燬
Vue.prototype.$destroy = function () { var vm = this; if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy'); ... vm.__patch__(vm._vnode, null); callHook(vm, 'destroyed'); ... };
由於觸發removeVnodes函數,是先父後子的,因此執行實例執行$destroy的時候,是父beforeDestroy > 子beforeDestroy,而後vm.__patch__(vm._vnode, null)又會遞歸去尋找他的子組件,去執行data.hook.destroy,因此子組件的destroyed鉤子會先執行,父組件後面執行
父beforeDestroy > 子beforeDestroy > 子destroyed > 父destroyed
這兩個鉤子的話是應用在keep-alive組件所包裹的組件下的,跟mounted和destroyed鉤子相似,再代碼判斷裏,經過vnode.data.keepLive來區分普通非緩存組件,進而執行不一樣的鉤子
這篇是總結了vue10個生命週期運行機制,若是你有幸看完了,若是有什麼不對的地方,請評論指出或私自探討一下,若是以爲不錯,點個贊吧。哈哈