刁钻面试官:vue 节点销毁的时候做了些什么?
2020/4/7 11:01:44
本文主要是介绍刁钻面试官:vue 节点销毁的时候做了些什么?,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
面试路上难免磕磕绊绊
但是没想到这次遇到狼灭了
"你知道 vue 节点销毁的时候做了些什么吗?"
"..."
vue生命周期
我们知道vue的生命周期有这些
var LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured' ]; 复制代码
其中'beforeDestroy', 'destroyed',是我们自己可以编写的生命周期函数
但是除此之外,其实vue还有一些钩子函数是内部使用的,其中也有destroy钩子
var hooks = ['create', 'activate', 'update', 'remove', 'destroy']; 复制代码
那么当一个节点销毁的时候,到底做了些啥呢,首先让我们来回顾一下vue中一个节点是怎么被触发销毁的
节点如何触发销毁?
当我们修改一个data的时候
由劫持的 set 触发了数据 deps 内所有关联的 watcher 的更新
接着触发组件的 update 从而进行新旧节点的 patch 进行相应的 dom 更新
当然 patch 函数的实现很复杂,有疑惑的同学可以去找找资料看看
今天我们这里只要知道在 patch 函数有三个地方会触发组件销毁就行了
1.节点删除或者替换
这个很好理解,比如我们使用 v-if 来控制一个组件是否存再
旧节点不存在了,这个当然得销毁掉
或者组件不是同一个 component 了,最常用的例子是 <router-view>,<component :is=""> 等等
这些组件会动态变换自己的组件类型,新的组件会创建出来,于是旧的就需要销毁了
2.删除children
接着 patch 进入第二阶段,patchVNode,这里会比对新旧节点是否都有 children
如果新节点没有子节点,说明是删除节点操作,则需要销毁 oldNode 的 children
3.children 内子节点被删除或者替换
最后是进入 updateChildren 阶段,也就是我们常说的 diff 阶段
diff 阶段会从新旧子节点的首尾进行一个循环
分别进首位,尾位,首尾,尾首判断
进行 patch ,新增,左右移动操作
当循环完成后,如果新子节点已经循环完了,旧子节点还没循环完
说明需要删除多余的节点
那么销毁的方法做了些什么呢?我们来看看源码
removeVnode函数
可以看到上边删除操作是调用的这个方法
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); 复制代码
remove & destroy 钩子
于是现在只要知道钩子函数内执行了什么就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是更新我们定义的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
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
再来看看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的动画
vue组件的destroy钩子
除了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~
这篇关于刁钻面试官:vue 节点销毁的时候做了些什么?的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南