Skip to content

图解双端diff

面试题:说一下 Vue3 中的 diff 相较于 Vue2 有什么变化?

  • Vue2: 双端diff
  • Vue3: 快速diff

1. diff的概念

diff 算法是用于比较两棵虚拟 DOM 树的算法,目的是找到它们之间的差异,并根据这些差异高效地更新真实 DOM,从而保证页面在数据变化时只进行最小程度的 DOM 操作。

思考🤔:为什么需要进行diff,不是已经有响应式了么?

答案:响应式虽然能够侦测到响应式数据的变化,但是只能定位到组件,代表着某一个组件要重新渲染。组件的重新渲染就是重新执行对应的渲染函数,此时就会生成新的虚拟 DOM 树。但是此时我们并不知道新树和旧树具体哪一个节点有区别,这个时候就需要diff算法来找到两棵树的区别。

20210301193804

2. diff算法的特点

  1. 分层对比:它会逐层对比每个节点和它的子节点,避免全树对比,从而提高效率。
  2. 相同层级节点对比:在进行 diff 对比的时候,Vue会假设对比的节点是同层级的,也就是说,不会做跨层的比较。
20210301203350

3. diff算法详细流程

  1. 从根节点开始比较,看是否相同。所谓相同,是指两个虚拟节点的标签类型key 值均相同,但 input 元素还要看 type 属性

    1. 相同
      • 相同就说明能够复用,此时就会将旧虚拟DOM节点对应的真实DOM赋值给新虚拟DOM节点
      • 对比新节点和旧节点的属性,如果属性有变化更新到真实DOM. 这说明了即便是对 DOM 进行复用,也不是完全不处理,还是会有一些针对属性变化的处理
      • 进入【对比子节点】
    2. 不相同
      • 如果不同,该节点以及往下的子节点没有意义了,全部卸载
        • 直接根据新虚拟DOM节点递归创建真实DOM,同时挂载到新虚拟DOM节点
        • 销毁旧虚拟DOM对应的真实DOM,背后调用的是 vnode.elm.remove( ) 方法
  2. 对比子节点:

    1. 仍然是同层做对比
    2. 深度优先
    3. 同层比较时采用的是双端对比
    image-20240906101143754

4. 双端对比

之所以被称之为双端,是因为有两个指针,一个指向头节点,另一个指向尾节点,如下所示:

image-20240913225147579

无论是旧的虚拟 DOM 列表,还是新的虚拟 DOM 列表,都是一头一尾两个指针。

接下来进入比较环节,整体的流程为:

  1. 步骤一:新头和旧头比较

    • 相同:

      • 复用 DOM 节点

        image-20240914101542039
      • 新旧头索引自增

        image-20240914101629244
      • 重新开始步骤一

    • 不相同:进入步骤二

  2. 步骤二:新尾和旧尾比较

    • 相同:

      • 复用 DOM 节点

        image-20240914101834010
      • 新旧尾索引自减

        image-20240914101913347
      • 重新开始步骤一

    • 不相同,进入步骤三

  3. 步骤三:旧头和新尾比较

    • 相同:

      • 说明可以复用,并且说明节点从头部移动到了尾部,涉及到移动操作,需要将旧头对应的 DOM 节点移动到旧尾对应的 DOM 节点之后

        image-20240914101231300
      • 旧头索引自增,新尾索引自减

        image-20240914101400686
      • 重新开始步骤一

    • 不相同,进入步骤四

  4. 步骤四:新头和旧尾比较

    • 相同:

      • 说明可以复用,并且说明节点从尾部移动到了头部,仍然涉及到移动操作,需要将旧尾对应的 DOM 元素移动到旧头对应的 DOM 节点之前

        image-20240914105559210
      • 新头索引自增,旧尾索引自减

        image-20240914105649208
      • 重新开始步骤一

    • 不相同:进入步骤五

  5. 暴力比较:上面 4 个步骤都没找到相同的,则采取暴力比较。在旧节点列表中寻找是否有和新节点相同的节点,

    • 找到

      • 说明是一个需要移动的节点,将其对应的 DOM 节点移动到旧头对应的 DOM 节点之前

        image-20240914110012627
      • 新头索引自增

        image-20240914110048026
      • 回到步骤一

    • 没找到

      • 说明是一个新的节点,创建新的 DOM 节点,插入到旧头对应的 DOM 节点之前

        image-20240914110332605
      • 新头索引自增

        image-20240914110401233
      • 回到步骤一

新旧节点列表任意一个遍历结束,也就是 oldStart > OldEnd 或者 newStart > newEnd 的时候,diff 比较结束。

  • 旧节点列表有剩余(newStart > newEnd):对应的旧 DOM 节点全部删除掉
  • 新节点列表有剩余(oldStart > OldEnd):将新节点列表中剩余的节点创建对应的 DOM,放置于新头节点对应的 DOM 节点后面

综合示例

当前旧 Vnode 和新 VNode 如下图所示:

image-20240914111038061
  1. 头头对比,能够复用,新旧头指针右移

    image-20240914111750328
  2. 头头不同,尾尾相同,能够复用,尾尾指针左移

    image-20240914111936261
  3. 头头不同,尾尾不同,旧头新尾相同,旧头对应的真实DOM移动到旧尾对应的真实DOM之后,旧头索引自增,新尾索引自减

    image-20240914112233100
  4. 头头不同,尾尾不同,旧头新尾不同,新头旧尾相同,旧尾对应的真实DOM移动到旧头对应的真实DOM之前,新头索引自增,旧尾索引自减

    image-20240914112710405
  5. 头头不同,尾尾不同,旧头新尾不同,新头旧尾不同,进入暴力对比,找到对应节点,将对应的真实DOM移动到旧头对应的真实DOM之间,新头索引自增

    image-20240914113000896
  6. 头头不同,尾尾不同,旧头新尾不同,新头旧尾相同,将旧尾对应的真实DOM移动到旧头对应的真实DOM之前,新头索引自增,旧尾索引自减

    image-20240914113247844
  7. 头头不同,尾尾不同,旧头新尾不同,新头旧尾不同,暴力对比发现也没找到,说明是一个全新的节点,创建新的DOM节点,插入到旧头对应的DOM节点之前,新头索引自增

    image-20240914113444878
  8. newEnd > newStart,diff 比对结束,旧 VNode 列表还有剩余,直接删除即可。

    image-20240914113721337

-EOF-

Released under the MIT License.