key的作用是让Diff算法准确识别同一节点:无key时按序比对易误判更新,有key则通过唯一标识复用节点状态;双端对比+哈希映射实现高效移动定位,移动操作用insertBefore而非先删后插,稳定key须源自数据本身而非index。

Key 的作用:让 Diff 算法识别“同一节点”
没有 key 时,React/Vue 等框架只能按顺序逐个比对子节点,一旦中间插入或删除一个元素,后续所有节点都会被误判为“需更新”,导致大量无谓的 DOM 替换。加上 key 后,算法能根据唯一标识(如 id、uuid)把新旧节点一一对应起来——哪怕位置变了,只要 key 相同,就认为是“同一个节点在移动”,从而复用其状态(比如 input 的焦点、滚动位置、组件内部 state)。
双端对比 + key 映射:高效定位移动节点
主流实现(如 React 18 的 Fiber reconciler 或 Vue 3 的 patchKeyedChildren)采用“双端比较 + 哈希映射”策略:
- 先比头尾:分别比较新旧列表的首尾节点(key 相同则直接复用,不移动 DOM)
- 构建旧节点 key → index 映射表:遍历旧子节点,存 {key: oldIndex},用于 O(1) 查找
- 遍历新节点,查映射表:若找到匹配 key,说明该节点存在,只需判断是否需要移动;若没找到,说明是新增节点,执行插入
- 记录移动位置:对需复用但位置变化的节点,记录其在新序列中的目标索引;最后统一用 insertBefore 插入到正确位置(而非 remove + append),避免重排抖动
DOM 插入与删除的真实操作链
关键点在于:**移动 ≠ 先删后插,而是 insertBefore + 节点复用**。例如旧序列为 [A, B, C](key 分别为 'a','b','c'),新序列为 [C, A, B]:
- C 在旧列表末尾(index=2),新列表开头(index=0)→ 需前移:调用 parent.insertBefore(C, A)
- A 原在 index=0,现为 index=1 → 无需移动(因 C 已前置,A 自然落到第二位)
- B 同理自然就位
- 全程无 removeChild;所有节点 DOM 实例都被保留,仅调整其在父容器内的顺序
删除操作只发生在旧节点 key 在新列表中完全消失时,才调用 removeChild;新增则是 createElement + append/insertBefore。
为什么不能只靠 index?Key 冲突与边界陷阱
用数组下标当 key(如 :key="index")是常见错误。一旦列表排序、过滤或增删,index 快速错位,key 失去唯一性与稳定性,导致:
- input 失焦(本该复用的节点被当作新节点重建)
- 列表项动画错乱(diff 认为每个都换了)
- useEffect 重复执行(组件被错误卸载又挂载)
真正稳定的 key 应来自数据本身(如 item.id),且在生命周期内不变。若数据无天然 id,应生成并持久化(如 useId 或服务端下发)。

















