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 中匹配,就算指针中间有可复用的节点,也不能被复用。
接下来,看一下上图做匹配的过程:

oldS与S匹配
将 DOM 中的节点 a 放到第一个。已经是第一个了就不管了。

olds与E匹配
将 DOM 中的节点 b 放到最后一个。

oldE与S匹配
本来是要将 c 移动到 S 对应的位置。可是真实 DOM 中节点c 已经是在第二个位置了。所以什么都不做。
oldS > oldE结束匹配。
将剩余的节点 d 按照自己的 index 插入到 DOM 中去。
匹配结束有两种情况。
oldS > oldE说明oldCh先遍历完,则需要将多余的Vch根据index添加到DOM中。S > E说明Vch先遍历完。则需要删除oldCh中多余的节点。























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)