Diff算法处理列表更新分三阶段:先双端比对剪除不变部分,再批量处理剩余节点的增删,仅当数量相等且顺序错乱时才启动基于key的移动优化;key必须稳定唯一,不可用index。

Diff 算法处理列表更新,核心不是“逐个判断每个节点怎么变”,而是分阶段收口、划清边界、再精准干预。新增、移动、删除这三类操作,并非并列执行,而是有明确的先后顺序和触发条件。
双端比对先“剪掉”不变部分
算法第一件事是同时从新旧列表的头和尾推进比对,只要节点类型相同且 key 一致,就直接复用、递归 patch,不移动也不重建。
- 开头连续匹配(如旧 [A,B,C,D] vs 新 [A,B,X,Y,Z])→ A、B 直接跳过,后续只处理 [C,D] 和 [X,Y,Z]
- 结尾连续匹配(如旧 [A,B,C,D] vs 新 [X,Y,A,B,C,D])→ C、D 直接跳过,剩余 [A,B] 和 [X,Y]
- 这一阶段不涉及 DOM 操作,只做索引收缩;多数真实场景中,增删集中在中间,所以这一步就规避了大半冗余计算
边界清晰后,一次性处理存在性变化
双端收口完成后,剩下未匹配的节点区间非常明确:
- 若旧列表还有剩余节点(但新列表已空)→ 这些节点在新结构中彻底消失,批量调用 removeChild
- 若新列表还有剩余节点(但旧列表已空)→ 全是全新元素,按序创建并 append 或 insertBefore
- 这个阶段不查 key、不重排、不移动;它只回答“有没有”,不回答“该放哪”
仅当数量相等且顺序错乱时,才启动移动优化
只有当新旧剩余节点数量相同、但顺序不一致(比如旧 [1,3,4,2] → 新 [1,2,3,4]),算法才进入移动逻辑:
- 构建 oldKeyToIndex 映射表:一次遍历旧列表,记录每个 key 最后出现的位置
- 生成 newIndexToOldIndexMap:遍历新列表,查每个节点在旧列表中的位置,找不到记为 0(代表新增)
- 在位置数组上运行 最长递增子序列(LIS):找出哪些新节点在旧列表中原本就保持相对顺序,这些节点无需移动,原位复用
- 其余节点,则通过 insertBefore 一次性插入到目标位置——全程不 remove,只调整父子关系
Key 不是可选项,而是移动判定的前提
没有 key,算法只能按索引硬对齐,一旦中间增删,后续所有节点都会被误判为“需更新”或“需重建”:
- input 失焦、滚动位置丢失、组件状态重置,本质都是 key 缺失或不稳定导致的节点误判
- 用 index 当 key 是高危操作:排序、筛选、分页后 index 变动,key 就失效
- 真正可靠的 key 必须来自数据本体,比如 item.id 或 item.uuid,且在整个生命周期内不变

















