Vue3用最长递增子序列(LIS)优化DOM Diff,通过构建新旧节点索引映射数组并求其LIS,确定最多可复用的不动节点,仅移动其余节点,将DOM操作降至最少,时间复杂度为O(n log n)。

最长递增子序列(Longest Increasing Subsequence,简称 LIS)不是指连续的一段,而是从原序列中**按原始顺序挑出若干元素**,使其严格递增,且长度尽可能长。它不关心元素是否相邻,只关注“顺序”和“大小关系”。Vue 3 把这个数学概念巧妙地用在 DOM 节点移动优化上:找出哪些节点可以不动,只移动其余节点,从而把真实 DOM 操作降到最少。
为什么 LIS 能减少 DOM 移动?
在列表更新时,如果新旧节点顺序发生乱序(比如旧顺序是 [A, B, C, D],新顺序是 [A, D, B, C]),直接逐个比对再移动容易重复操作。Vue 3 的思路是:先构建一个映射数组 newIndexToOldIndexMap,记录新列表中每个位置对应旧列表的索引(若无对应则为 0)。对这个映射数组求 LIS,就等于找出了**最多可以保留原位置、无需移动的节点集合**。
例如:
旧节点顺序(带索引):A(0), B(1), C(2), D(3)
新节点顺序:A, D, B, C → 对应旧索引数组为 [0, 3, 1, 2]
该数组的 LIS 是 [0, 1, 2](对应 A→B→C),说明这三个节点在新列表中的相对顺序与旧列表一致,只需把 D 插入到 A 后面即可,其他节点不动。
Vue 3 实际怎么算 LIS?用的是贪心 + 二分查找
Vue 3 没有用 O(n²) 的动态规划,而是采用时间复杂度仅 O(n log n) 的贪心策略,核心逻辑是维护一个“结果索引数组”,它不直接存数值,而是存原数组中满足条件的**下标**:
- 遍历 newIndexToOldIndexMap 中每个值(即旧索引)
- 跳过值为 0 的项(代表全新节点,不参与移动判断)
- 若当前值大于结果数组末尾对应位置的旧索引,就追加进去
- 否则用二分查找找到结果数组中第一个 ≥ 当前值的位置,并替换它
- 最后通过前驱数组反向追溯,还原出真正的 LIS 下标序列
这个过程不追求中间结果“看起来像原序列”,只要保证最终长度准确、且能支撑后续节点复用逻辑即可——Vue 关注的是“最少移动次数”,不是“展示最漂亮子序列”。
Vue 3 组合式 API 与 最佳实践,涵盖代码组织、useXxx 模式、组合式函数设计、Store 集成、响应式优化及功能抽取。
立即学习“前端免费学习笔记(深入)”;
LIS 结果如何指导 DOM 移动?
拿到 LIS 对应的旧节点索引后,Vue 就知道哪些节点可以原地复用。剩余节点分为两类:
- 需要挂载的新节点:newIndexToOldIndexMap 中值为 0 的位置,直接 create + insert
- 需要移动的旧节点:不在 LIS 中、但存在于旧列表里的节点,按新顺序依次 insertBefore 或 append 到正确位置
关键细节在于:Vue 不会把节点从 DOM 中 remove 再 append,而是用 insertBefore 原地调整位置,避免 layout thrashing;同时所有移动都基于已有节点引用,不触发重建。
对比 Vue 2:为什么 Vue 3 更优?
Vue 2 在乱序场景下依赖 key 映射表逐个查找,最坏情况 O(n²);遇到头尾不匹配又无 key 的节点,还可能降级为暴力遍历。Vue 3 的 LIS 方案把乱序处理统一收束为一次数学计算,无论多复杂的打乱,都能在 O(n log n) 内确定最优移动路径。实测在千级节点列表更新中,DOM 操作量平均降低 40% 以上,重排重绘更少,响应更顺滑。

















