面試路上不免磕磕絆絆
可是沒想到此次遇到狼滅了javascript
"你知道 vue 節點銷燬的時候作了些什麼嗎?"
"..."vue
咱們知道vue的生命週期有這些java
var LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured' ]; 複製代碼
其中'beforeDestroy', 'destroyed',是咱們本身能夠編寫的生命週期函數
可是除此以外,其實vue還有一些鉤子函數是內部使用的,其中也有destroy鉤子node
var hooks = ['create', 'activate', 'update', 'remove', 'destroy']; 複製代碼
那麼當一個節點銷燬的時候,到底作了些啥呢,首先讓咱們來回顧一下vue中一個節點是怎麼被觸發銷燬的面試
當咱們修改一個data的時候
由劫持的 set 觸發了數據 deps 內全部關聯的 watcher 的更新
接着觸發組件的 update 從而進行新舊節點的 patch 進行相應的 dom 更新緩存
固然 patch 函數的實現很複雜,有疑惑的同窗能夠去找找資料看看
今天咱們這裏只要知道在 patch 函數有三個地方會觸發組件銷燬就好了bash
這個很好理解,好比咱們使用 v-if 來控制一個組件是否存再
舊節點不存在了,這個固然得銷燬掉markdown
或者組件不是同一個 component 了,最經常使用的例子是 <router-view>,<component :is=""> 等等
這些組件會動態變換本身的組件類型,新的組件會建立出來,因而舊的就須要銷燬了dom
接着 patch 進入第二階段,patchVNode,這裏會比對新舊節點是否都有 children
若是新節點沒有子節點,說明是刪除節點操做,則須要銷燬 oldNode 的 children函數
最後是進入 updateChildren 階段,也就是咱們常說的 diff 階段
diff 階段會重新舊子節點的首尾進行一個循環
分別進首位,尾位,首尾,尾首判斷
進行 patch ,新增,左右移動操做
當循環完成後,若是新子節點已經循環完了,舊子節點還沒循環完
說明須要刪除多餘的節點
那麼銷燬的方法作了些什麼呢?咱們來看看源碼
能夠看到上邊刪除操做是調用的這個方法
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); } } } } 複製代碼
很簡單得亞子,其實就是作了兩個事情
執行平臺的刪除節點方法,而後執行remove鉤子和destroy鉤子
function removeAndInvokeRemoveHook (vnode, rm) { if (isDef(rm) || isDef(vnode.data)) { var i; var listeners = cbs.remove.length + 1; if (isDef(rm)) { // we have a recursively passed down rm callback // increase the listeners count rm.listeners += listeners; } else { // directly removing rm = createRmCb(vnode.elm, listeners); } // recursively invoke hooks on child component root node if (isDef(i = vnode.componentInstance) && isDef(i = i._vnode) && isDef(i.data)) { removeAndInvokeRemoveHook(i, rm); } for (i = 0; i < cbs.remove.length; ++i) { cbs.remove[i](vnode, rm); } if (isDef(i = vnode.data.hook) && isDef(i = i.remove)) { i(vnode, rm); } else { rm(); } } else { removeNode(vnode.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]); } } } 複製代碼
進而能夠發現,實際上兩個方法都很相似
都是遞歸調用本身和子節點的remove鉤子和destroy鉤子
其中remove稍有特殊,執行完鉤子函數以後,還會執行真正的平臺移除節點方法
removeNode(childElm);
複製代碼
因而如今只要知道鉤子函數內執行了什麼就ok了
咱們找到定義全部鉤子函數的地方,找到全部鉤子函數
var baseModules = [
ref,
directives
]
var platformModules = [
attrs,
klass,
events,
domProps,
style,
transition
]
複製代碼
這就是vue定義的 基礎module 和 平臺module
但並非全部 module 都須要在 remove 階段 和destroy 階段執行鉤子函數
咱們進一步查看代碼,會發現只有
ref,directives,transition 定義了remove或者destroy鉤子
其中ref是更新咱們定義的ref引用信息 destroy 鉤子是清除真實dom的引用,代碼以下
var ref = { create: function create (_, vnode) { ... }, update: function update (oldVnode, vnode) { ... }, destroy: function destroy (vnode) { registerRef(vnode, true); } } function registerRef (vnode, isRemoval) { ... if (isRemoval) { if (Array.isArray(refs[key])) { remove(refs[key], ref); } else if (refs[key] === ref) { refs[key] = undefined; } } else { ... } } 複製代碼
directives 則是更新咱們的自定義指令,從而觸發指令的 unbind 事件
function updateDirectives (oldVnode, vnode) { if (oldVnode.data.directives || vnode.data.directives) { _update(oldVnode, vnode); } } function _update (oldVnode, vnode) { var isCreate = oldVnode === emptyNode; ... if (!isCreate) { for (key in oldDirs) { if (!newDirs[key]) { // no longer present, unbind callHook$1(oldDirs[key], 'unbind', oldVnode, oldVnode, isDestroy); } } } } 複製代碼
再來看看transition,它只有remove鉤子 實際上就是就是執行
var transition = inBrowser ? { create: _enter, activate: _enter, remove: function remove$$1 (vnode, rm) { /* istanbul ignore else */ if (vnode.data.show !== true) { leave(vnode, rm); } else { rm(); } } } : {} 複製代碼
這裏能夠看到銷燬的時候,會觸發transition的leave方法
其中就會執行對應的離開動畫
leave方法在 show 指令的 update 方法中一樣會執行,能夠解釋爲何v-show也能觸發transition的動畫
除了modules的鉤子,固然vue組件本身的鉤子也是很重要的
在 createComponent 建立組件的時候,會執行installComponentHooks(data);
這裏就會將鉤子函數綁定到data.hook上
而後節點銷燬的時候,若是有這個鉤子就會執行
if (isDef(i = data.hook) && isDef(i = i.destroy)) { i(vnode); } 複製代碼
那麼咱們只要看看destroy裏作了什麼就好了
var componentVNodeHooks = { init: function init ( vnode, hydrating, parentElm, refElm ) { ... }, prepatch: function prepatch (oldVnode, vnode) { ... }, insert: function insert (vnode) { ... }, destroy: function destroy (vnode) { var componentInstance = vnode.componentInstance; if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy(); } else { deactivateChildComponent(componentInstance, true /* direct */); } } } }; 複製代碼
這裏能夠看到,組件銷燬的時候
就是執行了組件的$destroy函數
固然,若是組件是被keepAlive緩存的,就不會銷燬,只會進入deactive流程
其中銷燬作了這些操做
Vue.prototype.$destroy = function () { var vm = this; if (vm._isBeingDestroyed) { return } callHook(vm, 'beforeDestroy'); vm._isBeingDestroyed = true; // remove self from parent var parent = vm.$parent; if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) { remove(parent.$children, vm); } // teardown watchers if (vm._watcher) { vm._watcher.teardown(); } var i = vm._watchers.length; while (i--) { vm._watchers[i].teardown(); } // remove reference from data ob // frozen object may not have observer. if (vm._data.__ob__) { vm._data.__ob__.vmCount--; } // call the last hook... vm._isDestroyed = true; // invoke destroy hooks on current rendered tree vm.__patch__(vm._vnode, null); // fire destroyed hook callHook(vm, 'destroyed'); // turn off all instance listeners. vm.$off(); // remove __vue__ reference if (vm.$el) { vm.$el.__vue__ = null; } // release circular reference (#6759) if (vm.$vnode) { vm.$vnode.parent = null; } }; } 複製代碼
這裏首先觸發咱們本身編寫的beforeDestroy生命週期函數
而後清除vnode子節點,接着清除watchers全部對應的依賴
接下來這一段頗有意思
vm.__patch__(vm._vnode, null);
複製代碼
function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { if (isUndef(vnode)) { if (isDef(oldVnode)) { invokeDestroyHook(oldVnode); } return } ... } 複製代碼
這裏其實就是針對組件的遞歸調用銷燬,而後觸發destroyed生命週期函數
因此咱們組件的銷燬時間執行的順序爲:
父組件beforeDestroy -> 子組件beforeDestroy -> 子組件destroy ->父組件destroy
再下來執行$off函數
將_events置空
最後再清除相關節點的引用就結束了
因此,總結一下其實vue銷燬一個節點仍是作了很多操做的
會依次執行ref director transition 的remove或者destroy鉤子
而後執行vue組件銷燬方法
over~