面试路上不免磕磕绊绊
可是没想到此次遇到狼灭了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 来控制一个组件是否存再
旧节点不存在了,这个固然得销毁掉dom
或者组件不是同一个 component 了,最经常使用的例子是 <router-view>,<component :is=""> 等等
这些组件会动态变换本身的组件类型,新的组件会建立出来,因而旧的就须要销毁了函数
接着 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~