前端开发中,只要涉及到列表渲染,那么无论是 React 还是 Vue 框架,都会提示或要求每个列表项使用唯一的 key,那很多开发者就会直接使用数组的 index 作为 key 的值,而并不知道 key 的原理。那么这篇文章就会讲解 key 的作用以及为什么最好不要使用 index 作为 key 的属性值。
key 的作用Vue 中使用虚拟 dom 且根据 diff 算法进行新旧 DOM 对比,从而更新真实 dom ,key 是虚拟 DOM 对象的唯一标识, 在 diff 算法中 key 起着极其重要的作用。
key 在 diff 算法中的角色其实在 React,Vue 中 diff 算法大致是差不多,但是 diff 比对方式还是有较大差异的,甚至每个版本 diff 都大有不同。下面我们就以 Vue3.0 diff 算法为切入点,剖析 key 在 diff 算法中的作用
具体 diff 流程如下
Vue3.0 中 在 patchChildren 方法中有这么一段源码
if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { /* 对于存在 key 的情况用于 diff 算法 */ patchKeyedChildren( ... ) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { /* 对于不存在 key 的情况,直接 patch */ patchUnkeyedChildren( ... ) return } }
patchChildren 根据是否存在 key 进行真正的 diff 或者直接 patch。对于 key 不存在的情况我们就不做深入研究了。
我们先来看看一些声明的变量。
/* c1 老的 vnode c2 新的vnode */ let i = 0 /* 记录索引 */ const l2 = c2.length /* 新 vnode的数量 */ let e1 = c1.length - 1 /* 老 vnode 最后一个节点的索引 */ let e2 = l2 - 1 /* 新节点最后一个节点的索引 */
同步头部节点第一步的事情就是从头开始寻找相同的 vnode,然后进行 patch,如果发现不是相同的节点,那么立即跳出循环。
//(a b) c //(a b) d e /* 从头对比找到有相同的节点 patch ,发现不同,立即跳出*/ while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) /* 判断 key ,type 是否相等 */ if (isSameVNodeType(n1, n2)) { patch( ... ) } else { break } i++ }
流程如下:
isSameVNodeType 作用就是判断当前 vnode 类型 和 vnode 的 key 是否相等
export function isSameVNodeType(n1: VNode, n2: VNode): boolean { return n1.type === n2.type && n1.key === n2.key }
其实看到这,已经知道 key 在 diff 算法的作用,就是用来判断是否是同一个节点。
同步尾部节点第二步从尾开始同前 diff
//a (b c) //d e (b c) /* 如果第一步没有 patch 完,立即,从后往前开始 patch 如果发现不同立即跳出循环 */ while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch( ... ) } else { break } e1-- e2-- }
经历第一步操作之后,如果发现没有 patch 完,那么立即进行第二步,从尾部开始遍历依次向前 diff。如果发现不是相同的节点,那么立即跳出循环。流程如下:
添加新的节点第三步如果老节点是否全部 patch,新节点没有被 patch 完,创建新的 vnode
//(a b) //(a b) c //i = 2, e1 = 1, e2 = 2 //(a b) //c (a b) //i = 0, e1 = -1, e2 = 0 /* 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的 vnode 处理(这种情况说明已经 patch 完相同的 vnode ) */ if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch( /* 创建新的节点*/ ... ) i++ } } }
流程如下:
删除多余节点第四步如果新节点全部被 patch,老节点有剩余,那么卸载所有老节点
//i > e2 //(a b) c //(a b) //i = 2, e1 = 2, e2 = 1 //a (b c) //(b c) //i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } }
流程如下:
最长递增子序列到了这一步,比较核心的场景还没有出现,如果运气好,可能到这里就结束了,那我们也不能全靠运气。剩下的一个场景是新老节点都还有多个子节点存在的情况。那接下来看看,Vue3 是怎么做的。为了结合 move、新增和卸载的操作