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

最长递增子序列(LIS)在 Diff 算法中不直接“处理移动”,而是精准识别哪些节点**不需要移动**——剩下的,才是要动的。
先建位置映射:把新顺序翻译成旧坐标
算法不是看节点内容,而是看它们在旧列表里的“出身位置”:
- 遍历旧子节点,用 key 建立
oldKeyToIndex映射表(比如{a:0, b:1, c:2, d:3}) - 再遍历新子节点,对每个节点查它在旧列表中的最后索引;查不到就记为 0(代表全新节点)
- 最终得到一个数字数组,例如
[1, 0, 3, 2],含义是:“新列表第 0 位的节点,来自旧列表第 1 位;新列表第 1 位的节点,来自旧列表第 0 位……”
再求 LIS:找天然有序的“不动骨干”
这个位置数组里,严格递增的子序列(如 [0, 2] 或 [1, 3])意味着:对应的新节点,在旧 DOM 中原本就保持着“前面→后面”的相对顺序。
- LIS 不要求连续,只要下标在新列表中靠前、对应旧索引又更小,就说明它们“本来排得对”
- VUE3 用贪心 + 二分法快速构造 tails 数组,并通过前驱链反推出 LIS 所含的新节点下标(比如得出下标
[1, 3]) - 这些下标对应的新 vnode 就被标记为“可原位复用”,patch 时跳过移动,只更新 props 和子树
最后执行移动:只动该动的,且从后往前插
非 LIS 下标的节点,才进入 DOM 操作阶段:
- 从新列表末尾开始遍历(i 递减),逐个检查当前下标是否在 LIS 结果中
- 如果不在,就把它插入到正确位置(可能是 insertBefore,也可能是 move)
- 移动次数 = 剩余待处理节点数 − LIS 长度;这是理论最少移动次数,比暴力重排或 Vue2 的双端试探更优
不复杂但容易忽略:LIS 的价值不在“算得多炫”,而在把“哪些能留”这件事,变成可验证、可复现、可压测的确定性逻辑。

















