vue.js 3设计与实现 -- 双端 Diff 算法

上一章,我们介绍了简单 Diff 算法的实现原理。但是,简单 Diff 算法仍然存在很多缺陷,这些缺陷可以通过本章将要介绍的双端 Diff 算法解决。 

一、双端比较的原理

​ 简单 Diff 算法的问题在于,它对 DOM 的移动操作并不是最优的。我们拿上一章的例子来看, 如图 10-1 所示。

在这里插入图片描述

​ 显而易见只需要一次 DOM 移动操作即可完成更新。但简单 Diff 算法进行了两次 DOM 移动操作, 不过本章我们要介绍的双端 Diff 算法可以做到。接下来,我们就来讨论双端 Diff 算法的原理。

​ 顾名思义,双端 Diff 算法是一种同时对新旧两组子节点的两个端点进行比较的算法。因此, 我们需要四个索引值,分别指向新旧两组子节点的端点,如图 10-4 所示。

在这里插入图片描述

​ 用代码来表达四个端点,如下面 patchChildren 和 patchKeyedChildren 函数的代码所示:

function patchChildren(n1, n2, container) {
    
    if (typeof n2.children === 'string') {
    
        // 省略部分代码 
    } else if (Array.isArray(n2.children)) {
    
        // 封装 patchKeyedChildren 函数处理两组子节点 
        patchKeyedChildren(n1, n2, container) 
    } else {
    
        // 省略部分代码 
    } 
} 

function patchKeyedChildren(n1, n2, container) {
    
    const oldChildren = n1.children 
    const newChildren = n2.children 
    // 四个索引值 
    let oldStartIdx = 0 
    let oldEndIdx = oldChildren.length - 1 
    let newStartIdx = 0 
    let newEndIdx = newChildren.length - 1 
} 

​ 在上面这段代码中,我们将两组子节点的打补丁工作封装到了 patchKeyedChildren 函数中。 在该函数内,首先获取新旧两组子节点 oldChildren 和 newChildren,接着创建四个索引值,分别指向新旧两组子节点的头和尾,即 oldStartIdx、oldEndIdx、newStartIdx 和 newEndIdx。有了索引后,就可以找到它所指向的虚拟节点了,如下面的代码所示:

function patchKeyedChildren(n1, n2, container) {
    
    const oldChildren = n1.children 
    const newChildren = n2.children 
    // 四个索引值
    let oldStartIdx = 0 
    let oldEndIdx = oldChildren.length - 1 
    let newStartIdx = 0 
    let newEndIdx = newChildren.length - 1 
    // 四个索引指向的 vnode 节点 
    let oldStartVNode = oldChildren[oldStartIdx] 
    let oldEndVNode = oldChildren[oldEndIdx] 
    let newStartVNode = newChildren[newStartIdx] 
    let newEndVNode = newChildren[newEndIdx] 
} 

​ 其中,oldStartVNode 和 oldEndVNode 是旧的一组子节点中的第一个节点和最后一个节点, newStartVNode 和 newEndVNode 则是新的一组子节点的第一个节点和最后一个节点。有了这些信息之后,我们就可以开始进行双端比较了。怎么比较呢?如图 10-5 所示。

在这里插入图片描述

​ 在双端比较中,每一轮比较都分为四个步骤,如图 10-5 中的连线所示。

  • 第一步:比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的第一个子节点 p-4,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  • 第二步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的最后一个子节点 p-3,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  • 第三步:比较旧的一组子节点中的第一个子节点 p-1 与新的一组子节点中的最后一个子节点 p-3,看看它们是否相同。由于两者的 key 值不同,因此不相同,不可复用,于是什么都不做。
  • 第四步:比较旧的一组子节点中的最后一个子节点 p-4 与新的一组子节点中的第一个子 节点 p-4。由于它们的 key 值相同,因此可以进行 DOM 复用。

​ 找到了可复用的节点,该怎么移动呢?我们注意到,第四步是比较旧的一组子节点的最后一个子节点与新的一组子节点的第一个子节点,发现两者相同。这说明:节点 p-4 原本是最后一个子节点,但在新的顺序中,它变成了第一个子节点。换句话说,节点 p-4 在更新之后应该是第一个子节点。对应到程序的逻辑,可以将其翻译为:将索引 oldEndIdx 指向的虚拟节点所对应的真实 DOM 移动到索引 oldStartIdx 指向的虚拟节点所对应的真实 DOM 前面。如下面的代码所示:

function patchKeyedChildren(n1, n2, container) {
    
    const oldChildren = n1.children 
    const newChildren = n2.children 
    // 四个索引值 
    let oldStartIdx = 0 
    let oldEndIdx = oldChildren.length - 1 
    let newStartIdx = 0 
    let newEndIdx = newChildren.length - 1 
    // 四个索引指向的 vnode 节点 
    let oldStartVNode = oldChildren[oldStartIdx] 
    let oldEndVNode = oldChildren[oldEndIdx] 
    let newStartVNode = newChildren[newStartIdx] 
    let newEndVNode = newChildren[newEndIdx] 

    if (oldStartVNode.key === newStartVNode.key) {
    
        // 第一步:oldStartVNode 和 newStartVNode 比较 
    } else if (oldEndVNode.key === newEndVNode.key) {
    
        // 第二步:oldEndVNode 和 newEndVNode 比较 
    } else if (oldStartVNode.key === newEndVNode.key) {
    
        // 第三步:oldStartVNode 和 newEndVNode 比较 
    } else if (oldEndVNode.key === newStartVNode.key) {
    
        // 第四步:oldEndVNode 和 newStartVNode 比较 
        // 仍然需要调用 patch 函数进行打补丁 
        patch(oldEndVNode, newStartVNode, container) 
        // 移动 DOM 操作 
        // oldEndVNode.el 移动到 oldStartVNode.el 前面 
        insert(oldEndVNode.el, container, oldStartVNode.el) 

        // 移动 DOM 完成后,更新索引值,并指向下一个位置 
        oldEndVNode = oldChildren[--oldEndIdx] 
        newStartVNode = newChildren[++newStartIdx] 
    } 
} 

​ 在这段代码中,我们增加了一系列的 if…else if… 语句,用来实现四个索引指向的虚拟节点之间的比较。拿上例来说,在第四步中,我们找到了具有相同 key 值的节点。这说明,原来处于尾部的节点在新的顺序中应该处于头部。于是,我们只需要以头部元素 oldStartVNode.el 作为锚点,将尾部元素 oldEndVNode.el 移动到锚点前面即可。但需要注意的是,在进行 DOM 的移动操作之前,仍然需要调用 patch 函数在新旧虚拟节点之间打补丁。在这一步 DOM 的移动操作完成后,接下来是比较关键的步骤,即更新索引值。由于第四步中涉及的两个索引分别是 oldEndIdx 和 newStartIdx&#x

Vue.js 中,diff 算法是用于计算 Virtual DOM 中新旧节点之间差异的一种算法。它的主要作用是尽量减少 DOM 操作的次数,提高页面的渲染效率。 Vue.js 中的 diff 算法采用了一种双端比较的策略,即同时从新旧节点的首尾两端开始比较。具体步骤如下: 1. 首先,比较新旧节点列表的头节点是否相同,如果相同,则比较下一个节点,直到找到不同的节点为止。 2. 接着,比较新旧节点列表的尾节点是否相同,如果相同,则比较前一个节点,直到找到不同的节点为止。 3. 如果新节点列表已经比较完毕,而旧节点列表还有剩余,说明这些旧节点需要被删除,执行删除操作。 4. 如果旧节点列表已经比较完毕,而新节点列表还有剩余,说明这些新节点需要被添加,执行添加操作。 5. 如果新旧节点列表都还有剩余,说明需要进行节点的移动和更新操作。 1. 首先,从新旧节点列表的开头和结尾分别取出一个节点,然后进行比较。 2. 如果它们相同,说明不需要进行移动和更新操作,直接比较下一个节点。 3. 如果它们不相同,需要判断这些节点是否可以复用。 1. 如果可以复用,将旧节点移动到新节点的位置,并进行更新操作。 2. 如果不可以复用,直接将旧节点删除,并将新节点添加到对应的位置。 4. 重复上述步骤,直到新旧节点列表中的所有节点都被比较完毕。 需要注意的是,Vue.js 中的 diff 算法并不是完美的,它只是尽量减少 DOM 操作的次数,但不能保证每次都是最优解。因此,在实际开发中,需要合理地使用 Vue.js 的优化技巧,如避免在循环中使用复杂的计算和方法调用,减少不必要的 DOM 操作等,来提升页面的性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值