vue.js

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > vue.js > Vue2双端diff算法与Vue3快速diff算法

Vue2的双端diff算法与Vue3的快速diff算法详解

作者:Hello--_--World

文章详细介绍了Vue中的Diff算法,特别是Vue2的双端Diff和Vue3的快速Diff算法,双端Diff通过四个指针从两端向中间遍历,减少DOM操作,快速Diff通过预处理,减少参与复杂对比的节点,利用最长递增子序列优化乱序匹配,两者都利用节点的key值来优化更新过程,提升性能

Vue Diff算法原理

Diff 算法是 Vue 中虚拟 DOM(Virtual DOM)渲染器的核心。

其目标是:用最小的性能代价,找出新旧虚拟节点(VNode)之间的差异,并高效地更新真实 DOM。

1. Diff 算法的核心策略

为了将 O ( n 3 ) O(n^3) O(n3) 的通用树对比算法优化至 O ( n ) O(n) O(n),Vue 遵循了以下三个前提:

2. 核心执行流程:patch 函数

在源码中,Diff 过程主要由 patch 函数执行,其逻辑如下:

判断是否为相同节点

更新属性 (Props/Attrs)

对比子节点 (Children)

3. Vue 2 vs Vue 3 算法实现

Vue 2:双端 Diff (Double-Ended Diff)

Vue 2 使用四个指针分别指向新旧列表的头尾,进行四种假设性匹配

Vue 3:快速 Diff (Quick Diff)

Vue 3 借鉴了 inferno 算法,利用了 静态提升预处理

处理未知序列

4. 为什么 key 很重要?

注意:避免使用 index 作为 key。当列表发生排序、插入、删除操作时,index 的变化会导致 Vue 误判节点,产生不必要的 DOM 更新。

Vue 3:快速 Diff (Quick Diff) 详解

Vue 3 的快速 Diff 算法(Quick Diff)相比 Vue 2 的双端 Diff,最大的改进在于它通过预处理尽可能减少了需要参与复杂比对的节点数量,并利用数学上的最长递增子序列来计算出最少的 DOM 移动次数。

1. 预处理前置节点

定义 i 变量:记录当前前置索引值

2. 预处理后置节点

3. 处理仅有新增节点情况

仅有新增节点: i > e1 && i <= e2

4. 处理仅有卸载节点情况

5. 处理混合复杂情况(新增、卸载、移动)

完成 预处理前置节点、 预处理后置节点、到达这个状态

在这个状态下我们需要:

5.1 各个变量的作用

当前最远位置 (lastIndex / maxNewIndexSoFar):

移动标识 (moved):

新节点位置映射表 (keyToNewIndexMap):

新旧节点位置映射表 (source / newIndexToOldIndexMap):

值含义:

5.2 Diff 算法位置处理的详细流程

第一阶段:遍历新子序列,建立新节点位置映射表

第二阶段:遍历旧子序列,寻找可复用节点

遍历旧子序列(从 s 1 s1 s1 e 1 e1 e1),对每一个旧节点执行以下逻辑:

检查旧节点是否存在: 通过旧节点的 key 去 keyToNewIndexMap 中查找。

执行 Patch: 对新旧节点进行打补丁(更新属性、子节点等)。

填充 新旧节点位置映射表:

检测移动:

如果当前找到的新索引 < lastIndex,则说明该节点“跑到了前面节点的前面”,将 moved 设为 true。

第三阶段:移动与挂载(最核心)

一旦 moved 为 true,算法会执行以下操作:

计算最长递增子序列 (LIS): 针对 source 数组计算 LIS。

倒序遍历新旧节点位置映射表:

最长递增子序列算法

最长递增子序列算法

1. 定义

最长递增子序列是指在一个给定的序列中,找出一个子序列,使得子序列中的元素自左向右依次递增,且长度尽可能长。

2. 示例

假设输入数组:[10, 9, 2, 5, 3, 7, 101, 18]

3. 核心算法实现

方法一:动态规划 (Dynamic Programming)

这是最经典的方法,适合理解问题的本质。

/**
 * @param {number[]} nums
 * @return {number}
 */
function lengthOfLIS(nums) {
    if (!nums.length) return 0;
    
    // dp[i] 表示以 nums[i] 结尾的最长递增子序列的长度
    const dp = new Array(nums.length).fill(1);
    let maxLen = 1;

    for (let i = 1; i < nums.length; i++) {
        for (let j = 0; j < i; j++) {
            // 如果当前值大于前面的值,可以尝试拼接
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
        maxLen = Math.max(maxLen, dp[i]);
    }
    
    return maxLen;
}

方法二:贪心 + 二分查找 (Greedy + Binary Search)

二分查找(Binary Search)

贪心算法(Greedy Algorithm)

4 vue3 diff 算法中的最长递增子序列

4.1 二分查找 + 贪心算法 存在的问题

4.2 回溯修正

Vue 2 双端 Diff 算法详解

Vue 2 的 双端 Diff 算法 (Double-Ended Diff) 是基于 snabbdom 修改而来的。它的核心是通过四个指针同时从新旧两个列表的两端向中间遍历,尽可能地复用 DOM 节点。

1. 核心指针 (Four Pointers)

在 Diff 开始时,会定义四个索引变量:

2. 五步查找策略

算法在一个 while 循环中运行(条件:oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx)。每一步都会按顺序进行以下五种匹配:

① 头-头匹配 (oldStart vs newStart)

② 尾-尾匹配 (oldEnd vs newEnd)

③ 旧头-新尾匹配 (oldStart vs newEnd)

④ 旧尾-新头匹配 (oldEnd vs newStart)

⑤ 乱序匹配 (Key Map Lookup)

如果以上四种假设全都不成立:

生成映射表:建立旧列表所有节点的 { key: index } 哈希表。

查找:用 newStart 的 key 去表里查。

指针newStartIdx++。

3. 循环结束后的处理

头尾指针交叉循环结束,头指针 <= 尾指针 循环继续。

当其中一个列表遍历完时,循环停止:

旧列表先完 (oldStartIdx > oldEndIdx)

新列表先完 (newStartIdx > newEndIdx)

4. 为什么双端 Diff 更快?

注意:Vue 3 进一步引入了“静态标记”和“最长递增子序列”算法,处理乱序匹配的性能比 Vue 2 的双端 Diff 更加极致。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

您可能感兴趣的文章:
阅读全文