一、概述:

单纯的讲Diff算法,是一个比较宽泛的概念,并非Vue/React等独创。比如linux diff命令、git diff、JS对象间的diff等等,就是一个对比找差别的概念。

在虚拟DOM中,diff算法可以简单认为是在两棵树中找差别。

那为什么要用到Diff算法呢,不用可以么? 答案是如果不在乎性能,当然是可以的。对于一些简单的页面,DOM操作可能不影响体验。但是对于前端框架来说,将复杂的DOM操作转化成JS对象之间的对比,将DOM创建性能成本转嫁到JS对象上,从而实现DOM复用,进而得到高性能的渲染。

diff 算法的目的就是对比两次渲染结果,找到可复用的部分,然后剩下的该删除删除,该新增新增。

两棵树之间Diff,首先遍历两棵树,然后排序。所以时间复杂度是O(n3)。为了降低复杂度采用了:(1)只做同级比较,不做跨级比较(2)父级元素不相同就删除重建,不再进行深度比较(3)当元素和key二者都相同,可认为是相同节点,不再深度比较。同级比较,不进行深度比较,不较真,虽然简单粗暴,但是直接将时间复杂度O(n3)直接拉回了O(n)

二、React的Diff算法

2.1 Diff算法是什么

先有这样一个意识:Diff算法最终目的是使DOM元素充分复用,即能复用的元素直接复用;其次,能通过移动元素可以渲染的就移动元素;最不济的再创建新的DOM元素。

首先说一下Fiber,Fiber是React采用的新协调引擎,通过对大型复杂任务的分片、 对任务划分优先级,优先调度高优先级的任务、和可以对任务进行挂起、恢复、终止等操作,从而支持虚拟DOM的渐进式渲染。

在 Fiber 中,会把一个耗时很长的任务分成很多小的任务片,每一个任务片的运行时间很短。虽然总的任务执行时间依然很长,但是在每个任务小片执行完之后,都会给其他任务一个执行机会。这样,唯一的线程就不会被独占,其他任务也能够得到执行机会。

Fiber结构如下

{ 
    tag:TypeOfWork,//标识fiber类型    
    type:'div',//和fiber相关的组件类型 
    return:Fiber|null,//父节点 
    child:Fiber|null,//子节点 
    sibling:Fiber|null,//同级节点 
    alternate:Fiber|null,//diff的变化记录在这个节点上
    ... 
 }

举个例子

React的Diff算法

也就是说:整体渲染流程分成了两个阶段:

  • render 阶段:从 vdom 转换成 fiber,并且对需要 dom 操作的节点打上 effectTag 的标记
  • commit 阶段:对有 effectTag 标记的 fiber 节点进行 dom 操作,并执行所有的 effect 副作用函数。

2.2 Diff步骤

React Diff借用了深度优先遍历的思想,其主要步骤:

  • 首先,在Fiber构成的map中从左向右新老节点进行比对查找能复用的旧节点,如果有新老节点比对不成功的,则停止这一轮的比对,并记录了停止位置。

  • 如果上一轮比对能把所有的新节点都比对完毕,则删除旧节点还没比对的节点。

  • 反之,则继续从上一轮的停止位置继续开始循环新节点,拿每一个新节点去老节点fiber里面进行查找,有匹配成功的则复用,没匹配成功的则在协调位置的时候打上 Placement 的标记。

  • 在所有新节点比对完毕之后,检查还有没有没进行复用的旧节点,如果有,则全部删除。

文章参考:

1、为什么 React 的 Diff 算法不采用 Vue 的双端对比算法?

2、霍春阳.Vue.js设计与实现[P] .人民邮电出版社. 2022