参考原文-掘金-详解 vue 的 diff 算法
key
的特殊属性,主要用于 Vue
的虚拟 DOM
算法。在新旧 vnode
的对比中,如果不使用 key
,Vue
会最大限度的减少动态元素并尽可能的就地复用。这会导致一些渲染错误。而且当我们想要触发一些 transition
过渡动画的时候,会出现不生效的情况。因为 vue
判断该元素并没有改变。
而使用 key
的时候,它会基于 key
的变化,重新计算排序元素序列。并且会移除 key
不存在的元素。
其原理在于 Vue
的 diff
算法。而我们的 key
起作用在其 patch
的过程。
function patch(oldVnode, vnode) {
// some code
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode);
} else {
const oEl = oldVnode.el; // 当前oldVnode对应的真实元素节点
let parentEle = api.parentNode(oEl); // 父元素
createEle(vnode); // 根据Vnode生成新元素
if (parentEle !== null) {
api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)); // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el); // 移除以前的旧元素节点
oldVnode = null;
}
}
// some code
return vnode;
}
// 作者:windlany
// 链接:https://juejin.im/post/6844903607913938951
同层比较
如果两个节点是一样的,就执行 patchVnode()
方法进一步比较。
如果两个节点不一样,直接用新的 Vnode
替换旧的。如果两个父节点不一样,但是其子节点都是一样的,也不会进行子节点比较。这就是同层比较。
patchNode()
patchVnode (oldVnode, vnode) {
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
api.setTextContent(el, vnode.text)
}else {
updateEle(el, vnode, oldVnode)
if (oldCh && ch && oldCh !== ch) {
updateChildren(el, oldCh, ch)
}else if (ch){
createEle(vnode) //create el's children dom
}else if (oldCh){
api.removeChildren(el)
}
}
}
// 作者:windlany
// 链接:https://juejin.im/post/6844903607913938951
以上就是根据不同情况进行不同处理了。
- 如果新旧节点指向同一个对象,直接
return
什么都不做。 - 如果都有文本节点,并且不一样,则用新的替换旧的。
- 如果
oldVnode
有子节点,而新的Vnode
没有,则删除该子节点。 - 反过来,如果
Vnode
有子节点,而oldVnode
没有,则将该子节点添加。 - 如果都有子节点,则进行
updateChildren()
比较。
diff
算法就在 updateChildren()
函数里。
updateChildren()
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
// oldS 与 S 比较
}else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// oldE 与 E 比较
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// oldS 与 E 比较
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// oldE 与 S 比较
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}else {
// 使用key时的比较
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
这个函数主要做了以下事情:
- 将
Vnode
的子节点VnodeChildren(下文称Vch)
和oldNode
的子节点oldNodeChildren(下文称oldCh)
提取出来。 Vch
和oldCh
各有两个头尾的变量startIdx
和endIdx
。 他们的两个变量相互比较。一共有 4 种比较方式。如果 4 种都没有匹配,再看是否设置了key
。如果设置了,就会用key
进行比较。在比较的过程中,变量会往中间靠,一旦startIdx > endIdx
表明oldCh
和Vch
至少有一个已经遍历完了,就会结束比较。
接下来上图:
以下是 Vnode
和 oldVnode
将其取出来,并分别赋予头尾变量。
oldS
将会与 S
和 E
做 sameNode
比较。oldE
将会与 S
和 E
做 sameNode
比较。
- 一旦有一对匹配上了,那么真实的
DOM
会移动到与之对应的节点。这两个指针会像中间移动。 - 如果 4 组都没有匹配上,分两种情况。
- 如果新旧子节点都存在
key
, 那么会根据oldCh
的key
生成一张hash表
。用S
的key
与之做对比。匹配成功就去判断这S
和 该节点是否sameNode
。如果是,就在真实DOM
中将成功的节点移到最前面。否则将S
对应生成的节点插入到DOM
中对应的oldS
位置。S
指针向中间移动,被匹配old
中的节点置为null
。 - 如果没有
key
, 则直接将S
生成新的节点插入真实DOM
。
- 如果新旧子节点都存在
也就是,没有 key
只会做 4 中匹配,就算指针中间有可复用的节点,也不能被复用。
接下来,看一下上图做匹配的过程:
将 DOM
中的节点 a
放到第一个。已经是第一个了就不管了。
将 DOM
中的节点 b
放到最后一个。
本来是要将 c
移动到 S
对应的位置。可是真实 DOM
中节点c
已经是在第二个位置了。所以什么都不做。
将剩余的节点 d
按照自己的 index
插入到 DOM
中去。
匹配结束有两种情况。
oldS > oldE
说明oldCh
先遍历完,则需要将多余的Vch
根据index
添加到DOM
中。S > E
说明Vch
先遍历完。则需要删除oldCh
中多余的节点。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!