问题背景
在日常开发过程中,
当使用循环渲染的时候就一定要为循环项设置 key 属性,
那么为什么循环项就一定要有 key 呢?
如果 key 直接用 index 或者 random 赋值会有什么问题呢?
究其根本
无论是 Vue 还是 React 在每次触发渲染重绘的时候,
都会执行 diff 算法。
来判断新旧节点是否相同 sameVnode 的方法。
- 如果认为节点相同
递归 patchVnode 方法,
去处理新旧节点的 children,
根据新旧节点的 children 情况,
来对应 updateChildren、removeChildren 处理。
- 如果认为节点不同。
则直接销毁旧节点,创建新节点。
如果判断节点相同
源码位置:src/core/vdom/patch.js
那么是如何判断节点相同的。
如果排列组合去对比两套节点,
那么时间复杂度将会是 O(n^3)。
这种情况是没办法应用于生产环境的。
所以无奈之下,
想到了一个巧妙且折中的办法:
- 只会做同级的判断
- 只判断两个节点的 tag 和 key (还会判断 input 的 type)
这样虽然可能还是会有一些误判的可能性,
不过这样就可以将时间复杂度降低为 O(n),
且误判的造成资源浪费的情况是在我们可接受的范围内的,
所以这种方法还是可以应用生产环境的。
key 如何赋值
那么回到我们最初的问题,应该如何科学的对 key 赋值?
最正确的方法就是在设计数据结构的时候,
给数组的每一项都设置唯一的 ID(UUID)。
将这个 id 赋值给 key。
那么如果给 key 赋值 index 或者 random 会有什么问题呢?
- 如果给 key 赋值 index
那么在列表数据没有改变的时候,是没有问题的。
可是如果对数据有插入操作,问题将会出现。
比如在数组的 3 位置上插入新的数据。
那么再次触发渲染。3 位置上的新旧两个节点 tag 和 key 都相同。
sameVnode 会返回是 true 认为是相同节点。
接下来开始判断这个“相同节点”内部的 children。
结果就会发现内部是完全不一样的。
那么每个子节点都需要销毁再全部新建。
不仅如此,在 3 位置以后的节点也都会出现如此问题。
这样销毁再重建,
失去了优化 DOM 操作的初衷。
- 如果给 key 赋值 random
那么每次触发渲染刷新的时候,
sameVnode 永远都会返回是 false。
新旧节点都会认为是不同的节点。
所以每次渲染都会全部销毁,
再全部新建,
也同样失去了优化 DOM 操作的初衷。
后记
key 值的作用主要就是为了优化 diff 算法,
进而高效的更新虚拟 DOM。
其原理就是通过 key 值来精准判断新旧节点是否为相同节点。
从而避免去频繁更新 DOM 元素,
使得整个 patch 过程更加高效,减少 DOM 操作,提升性能。