关于VUE+element组件销毁思考和源码分析
WEB基础 6月 22, 2020

「吾日三省吾身。」
问题的复盘
上午同事提出一个问题,大概的意思是:弹窗组件在关闭重新打开之后,并没有触发数据的更新。
处理这个问题的时候,我的反应是抽象出来看,看似是数据绑定的问题,实际上是组件渲染的问题。
简单点来说就是如何重置组件,或者强制刷新组件
找到问题的根源当时想到了三种可行的解决办法:
v-if
强制更新
destroy
先说第一种,简单粗暴好理解,但是要频繁切换状态,不细说了。
第二种,其实我忘记具体的指令名字了,后来才查到了,当时就说了个$set
,当然原理是一样的,这个也不细说,可以看vue的文档
第三种,再次原谅我记不住方法名,只记得element有个destroy,现成的。同事提到了具体的方法名destroyOnClose
事情到此应该已经得到了解决,超哥@Alex提出了一个问题:在Element UI中这个方法是怎么实现的?
源码分析
先看源码
看到这似乎有种似曾相识的感觉,又有点困惑,:key
这个经常用,在v-for
中。在这里是什么意思?怎么++就能触发销毁和更新?
Key的说明
Vue对于Key的说明:
key 的特殊属性主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。
React对于Key的说明:
当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。
总结:
新子节点根据自己的key找到对应的旧子节点。
通俗的讲:
给子组件或者元素标签加上一个记号,方便vue根据记号对子组件或者元素标签做排查。一旦某个加上记号的子组件或者元素标签其上的记号进行了改变,vue就会对其做出一些相关的操作。
执行什么操作呢:
使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除 key 不存在的元素。
使用场景:
它也可以用于强制替换元素/组件而不是重复使用它。
- 完整地触发组件的生命周期钩子
- 触发过渡
这样就很好理解了,我们给每一个弹窗组件绑定了唯一key,当我们需要销毁这个弹窗的时候只需要修改组件的key,就可以让vue帮我们回收掉它,最简单的改值方式当然就是++(当然你也可以--,其实这有bug,有数值越界的问题)
VNode的说明
可能有人会问,vue底层是如何通过key干掉这个DOM的呢?这里就不得不提两个东西,一个叫VNode
,一个叫diff
这里简单的说一下有key的虚拟DOM更新,如果感兴趣的话,可以找我讨论。
VNode
虚拟DOM简而言之就是,用JS去按照DOM结构来实现的树形结构对象,一般称之为虚拟节点(VNode)
简单的diff算法
加key的h函数,
// 旧 children
[
h('li', { key: 'a' }, 1),
h('li', { key: 'b' }, 2),
h('li', { key: 'c' }, 3)
]
// 新 children
[
h('li', { key: 'c' }, 3)
h('li', { key: 'a' }, 1),
h('li', { key: 'b' }, 2)
]
export function h(tag, data = null, children = null) {
// 省略...
// 返回 VNode 对象
return {
// 省略...
key: data && data.key ? data.key : null
// 省略...
}
}
有了key,我们就可以找到相同可复用的DOM
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
const nextVNode = nextChildren[i]
// 遍历旧的 children
for (let j = 0; j < prevChildren.length; j++) {
const prevVNode = prevChildren[j]
// 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
if (nextVNode.key === prevVNode.key) {
patch(prevVNode, nextVNode, container)
break // 这里需要 break
}
}
}
这段代码中有两层嵌套的 for 循环语句,外层循环用于遍历新 children,内层循环用于遍历旧 children,其目的是尝试寻找具有相同 key 值的两个节点,如果找到了,则认为新 children 中的节点可以复用旧 children 中已存在的节点,这时我们仍然需要调用 patch 函数对节点进行更新,如果新节点相对于旧节点的 VNodeData 和子节点都没有变化,则 patch 函数什么都不会做
找到不动的,那么要动的怎么移动呢?
// 用来存储寻找过程中遇到的最大索引值
let lastIndex = 0
// 遍历新的 children
for (let i = 0; i < nextChildren.length; i++) {
const nextVNode = nextChildren[i]
// 遍历旧的 children
for (let j = 0; j < prevChildren.length; j++) {
const prevVNode = prevChildren[j]
// 如果找到了具有相同 key 值的两个节点,则调用 `patch` 函数更新之
if (nextVNode.key === prevVNode.key) {
patch(prevVNode, nextVNode, container)
if (j < lastIndex) {
// 需要移动
// refNode 是为了下面调用 insertBefore 函数准备的
const refNode = nextChildren[i - 1].el.nextSibling
// 调用 insertBefore 函数移动 DOM
container.insertBefore(prevVNode.el, refNode)
} else {
// 更新 lastIndex
lastIndex = j
}
break // 这里需要 break
}
}
}
如果我要添加怎么办呢?
for (let i = 0; i < nextChildren.length; i++) {
const nextVNode = nextChildren[i]
let find = false
for (let j = 0; j < prevChildren.length; j++) {
const prevVNode = prevChildren[j]
if (nextVNode.key === prevVNode.key) {
find = true
patch(prevVNode, nextVNode, container)
if (j < lastIndex) {
// 需要移动
const refNode = nextChildren[i - 1].el.nextSibling
container.insertBefore(prevVNode.el, refNode)
break
} else {
// 更新 lastIndex
lastIndex = j
}
}
}
if (!find) {
// 挂载新节点
// 找到 refNode
const refNode =
i - 1 < 0
? prevChildren[0].el
: nextChildren[i - 1].el.nextSibling
mount(nextVNode, container, false, refNode)
}
}
节点li-d在旧的children中是不存在的,所以当我们尝试在旧的children中寻找li-d节点时,是找不到可复用节点的,这时就没办法通过移动节点来完成更新操作,所以我们应该使用mount函数将li-d节点作为全新的VNode挂载到合适的位置。
那么最后我们再来实现一下删除
// 移除已经不存在的节点
// 遍历旧的节点
for (let i = 0; i < prevChildren.length; i++) {
const prevVNode = prevChildren[i]
// 拿着旧 VNode 去新 children 中寻找相同的节点
const has = nextChildren.find(
nextVNode => nextVNode.key === prevVNode.key
)
if (!has) {
// 如果没有找到相同的节点,则移除
container.removeChild(prevVNode.el)
}
}