lis在diff算法中不处理移动,而是精准识别可原位复用的节点:先构建新节点在旧列表中的位置数组(如[1,0,3,2]),再用贪心+二分求其最长递增子序列,所得下标对应无需移动的节点,其余才执行移动操作。

最长递增子序列(LIS)在 Diff 算法中不负责“执行移动”,而是精准锁定哪些节点可以原位复用——剩下的,才是需要移动的。
它先翻译顺序,而不是直接比内容
算法不看节点长什么样,只关心它们在旧列表里的“出身位置”:
- 遍历旧子节点,用 key 建立 oldKeyToIndex 映射表(如 {a:0, b:1, c:2})
- 再遍历新子节点,查每个节点在旧列表中的最后索引;查不到就记为 0(代表全新节点)
- 最终得到一个数字数组,例如 [1, 0, 3, 2]:意思是“新列表第 0 位来自旧列表第 1 位,第 1 位来自旧列表第 0 位……”
它找的是“天然有序的骨干”,不是完整排序
这个位置数组里,严格递增的子序列(如 [0, 2] 或 [1, 3])意味着:对应的新节点,在旧 DOM 中原本就保持着“前面→后面”的相对顺序。
- LIS 不要求连续,只要下标靠前、对应旧索引又更小,就说明它们“本来排得对”
- VUE3 用贪心 + 二分快速构造 tails 数组,并通过前驱链反推出 LIS 所含的新节点下标(比如得出下标 [1, 3])
- 这些下标对应的新 vnode 就被标记为“可原位复用”,patch 时跳过移动,只更新 props 和子树
它让移动变成“只动该动的”,且从后往前插
非 LIS 下标的节点,才进入 DOM 操作阶段:
- 从新列表末尾开始遍历(i 递减),逐个检查当前下标是否在 LIS 结果中
- 如果不在,就把它插入到正确位置(可能是 insertBefore,也可能是 move)
- 移动次数 = 剩余待处理节点数 − LIS 长度;这是理论最少移动次数,比暴力重排或 Vue2 的双端试探更优
它依赖 key,但 key 必须可靠
没有 key,算法只能按索引硬对齐,一旦中间增删,后续所有节点都会被误判为“需更新”或“需重建”:
- input 失焦、滚动位置丢失、组件状态重置,本质都是 key 缺失或不稳定导致的节点误判
- 用 index 当 key 是高危操作:排序、筛选、分页后 index 变动,key 就失效
- 真正可靠的 key 必须来自数据本体,比如 item.id 或 item.uuid,且在整个生命周期内不变











