React、Vue2、Vue3 三者的 diff 算法有什么区别

244 阅读9分钟

React、Vue2、Vue3 三者的 diff 算法有什么区别

认识 React Diff 算法

React 是 Fiber 架构的,Fiber 其实是一个链表的结构,但是由于没有设置反向指针,因此没有使用双端比对的方式去优化 Diff 算法(没有反向指针,从右往左遍历链表会很困难)。这一点在 React 源码 reconcileChildrenArray 函数的注释中也有说明。

React 采用 Fiber 架构的原因是 JavaScript 的运行会阻塞页面的渲染,React 为了不阻塞页面的渲染,采用了 Fiber 架构,Fiber 也是一种链表的数据结构,基于这个数据结构可以实现由原来不可中断的更新过程变成异步的可中断的更新。

Fiber 节点的定义:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  // Effects
  this.flags = NoFlags;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;

  // 省略一些无关的属性...
}

下面来分析一下 FiberNode 中各个属性的作用。

  • tag ,表示节点类型的标记,例如 FunctionComponent 、ClassComponent 等。

    tag 为 WorkTag 类型,WorkTag 的完整定义为

    export const FunctionComponent = 0;
    export const ClassComponent = 1;
    // Before we know whether it is function or class
    export const IndeterminateComponent = 2// Root of a host tree. Could be nested inside another node.
    export const HostRoot = 3// A subtree. Could be an entry point to a different renderer.
    export const HostPortal = 4export const HostComponent = 5;
    export const HostText = 6;
    export const Fragment = 7;
    export const Mode = 8;
    export const ContextConsumer = 9;
    export const ContextProvider = 10;
    export const ForwardRef = 11;
    export const Profiler = 12;
    export const SuspenseComponent = 13;
    export const MemoComponent = 14;
    export const SimpleMemoComponent = 15;
    export const LazyComponent = 16;
    export const IncompleteClassComponent = 17;
    export const DehydratedFragment = 18;
    export const SuspenseListComponent = 19;
    export const FundamentalComponent = 20;
    export const ScopeComponent = 21;
    export const Block = 22;
    export const OffscreenComponent = 23;
    export const LegacyHiddenComponent = 24;
    
  • key ,节点的唯一标识符,用于进行节点的 diff 和更新。

  • elementType ,大部分情况与 type 相同,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹,表示元素的类型

  • type ,表示元素的类型, 对于 FunctionComponent,指函数本身,对于ClassComponent,指 class,对于 HostComponent,指 DOM 节点 tagName

  • stateNode ,FiberNode 对应的真实 DOM 节点

  • return ,指向该 FiberNode 的父节点

  • child ,指向该 FiberNode 的第一个子节点

  • sibling ,指向右边第一个兄弟 Fiber 节点

  • index ,当前节点在父节点的子节点列表中的索引

  • ref ,存储 FiberNode 的引用信息,与 React 的 Ref 有关。

  • pendingProps ,表示即将被应用到节点的 props 。当父组件发生更新时,会将新的 props 存储在 pendingProps 中,之后会被应用到节点。

  • memoizedProps,表示节点上一次渲染的 props 。在完成本次更新之前,memoizedProps 中存储的是上一次渲染时的 props ,用于对比新旧 props 是否发生变化。

  • updateQueue,用于存储组件的更新状态,比如新的状态、属性或者 context 的变化。通过 updateQueue ,React 可以跟踪组件的更新并在合适的时机执行更新。

  • memoizedState ,类组件保存上次渲染后的 state ,函数组件保存的 hooks 信息。

  • dependencies ,存储节点的依赖信息,用于处理 useEffect 等情况。

  • mode ,表示节点的模式,如 ConcurrentMode 、StrictMode 等。

以下是关于节点副作用(Effect)的属性:

  • flags ,存储 FiberNode 的标记,表示节点上的各种状态和变化(删除、新增、替换等)。

    export type Flags = number;
    
    // FiberNode 中的所有 `flags` 定义如下
    
    export const NoFlags = /*                      */ 0b000000000000000000;
    export const PerformedWork = /*                */ 0b000000000000000001;
    
    export const Placement = /*                    */ 0b000000000000000010;
    export const Update = /*                       */ 0b000000000000000100;
    export const PlacementAndUpdate = /*           */ 0b000000000000000110;
    export const Deletion = /*                     */ 0b000000000000001000;
    export const ContentReset = /*                 */ 0b000000000000010000;
    export const Callback = /*                     */ 0b000000000000100000;
    export const DidCapture = /*                   */ 0b000000000001000000;
    export const Ref = /*                          */ 0b000000000010000000;
    export const Snapshot = /*                     */ 0b000000000100000000;
    export const Passive = /*                      */ 0b000000001000000000;
    
    export const PassiveUnmountPendingDev = /*     */ 0b000010000000000000;
    export const Hydrating = /*                    */ 0b000000010000000000;
    export const HydratingAndUpdate = /*           */ 0b000000010000000100;
    
    export const LifecycleEffectMask = /*          */ 0b000000001110100100;
    
    export const HostEffectMask = /*               */ 0b000000011111111111;
    
    export const Incomplete = /*                   */ 0b000000100000000000;
    export const ShouldCapture = /*                */ 0b000001000000000000;
    export const ForceUpdateForLegacySuspense = /* */ 0b000100000000000000;
    
    export const PassiveStatic = /*                */ 0b001000000000000000;
    
    export const BeforeMutationMask = /*           */ 0b000000001100001010;
    export const MutationMask = /*                 */ 0b000000010010011110;
    export const LayoutMask = /*                   */ 0b000000000010100100;
    export const PassiveMask = /*                  */ 0b000000001000001000;
    
    export const StaticMask = /*                   */ 0b001000000000000000;
    
    export const MountLayoutDev = /*               */ 0b010000000000000000;
    export const MountPassiveDev = /*              */ 0b100000000000000000;
    
  • nextEffect ,指向下一个有副作用的节点。

  • firstEffect,指向节点的第一个有副作用的子节点。

  • lastEffect,指向节点的最后一个有副作用的子节点。

以下关于优先级相关的属性

  • lanes ,代表当前节点上的更新优先级。
  • childLanes ,代表子节点上的更新优先级。
  • alternate,指向当前节点在上一次更新时的对应节点。 那为啥 Vue 不需要 Fiber 架构呢?那也是跟 Vue 本身的架构有关系,因为 Vue 采用了响应式数据,响应式数据可以细粒度地实现更新,即可以实现仅更新绑定了变更数据的那部分视图。那是不是说明 Vue 就比 React 强?那也不是,因为实现响应式的数据也是要耗费资源的,只能说是两个框架各自权衡取舍后的结果。

由于 React 无法使用双端比对的方法来优化 Diff 算法,所以 React 在进行多节点 Diff 的时候需要进行两轮遍历。

第一轮遍历:处理 更新 的节点。

第二轮遍历:处理剩下的不属于 更新 的节点。

Vue2 与 React Diff 的区别

认识 Vue2 Diff 算法

Vue2 采用了双端 Diff 算法,算法流程主要是:

  1. 对比头头、尾尾、头尾、尾头是否可以复用,如果可以复用,就进行节点的更新或移动操作。
  2. 如果经过四个端点的比较,都没有可复用的节点,则将就的子序列保存为节点 key 为 key ,index 为 value 的 map 。
  3. 拿新的一组子节点的头部节点去 map 中查找,如果找到可复用的节点,则将相应的节点进行更新,并将其移动到头部,然后头部指针右移。
  4. 然而,拿新的一组子节点中的头部节点去旧的一组子节点中寻找可复用的节点,并非总能找到,这说明这个新的头部节点是新增节点,只需要将其挂载到头部即可。
  5. 经过上述处理,最后还剩下新的节点就批量新增,剩下旧的节点就批量删除。

区别

Vue2 的 Diff 与 React 的 Diff 主要区别为:

  1. Vue2 采用双端比对的方式优化了 Diff 算法,而 React 由于是 Fiber 架构,是单链表,没有使用双端比对的方式优化
  2. Vue2 在 Diff 的时候与 React 在 Diff 的时候都采用了 map 来加快查找的效率,但是 Vue2 构造的 Diff 是 key -> index 的映射,而 React 构造的 Diff 是 key -> Fiber节点 的映射

Vue3 与 React Diff 的区别

认识 Vue3 Diff 算法

Vue3 的 Diff 算法与 Vue2 的 Diff 算法一样,也会先进行双端比对,只是双端比对的方式不一样。Vue3 的 Diff 算法借鉴了字符串比对时的双端比对方式,即优先处理可复用的前置元素和后置元素。

Vue3 的 Diff 算法的流程如下

  1. 处理前置节点
  2. 处理后置节点
  3. 新节点有剩余,则挂载剩余的新节点
  4. 旧节点有剩余,则卸载剩余的旧节点
  5. 乱序情况(新、旧节点都有剩余),则构建最长递增子序列
  6. 节点在最长递增子序列中,则该节点不需移动
  7. 节点不在最长递增子序列中,则移动该节点

区别

Vue3 的 Diff 与 React 的 Diff 主要区别为:

  1. Vue3 也采用了双端对比的方式优化了 Diff 算法,这一点与 Vue2 是类似的。
  2. Vue3 还构造了最长递增子序列,最大程度降低了 DOM 操作,而 React 没有使用最长递增子序列来加速 Diff 算法

总结

  • Vue2 、Vue3 都采用了双端比对的方式来加速 Diff 算法,
  • 而 React 采用了单链表的数据结构来存储 Fiber 节点,导致其使用双端比对的方式来优化 Diff 算法变得困难,所以 React 没有采用双端比对的方式来优化,
  • 相比 Vue2 ,Vue3 采用了最长递增子序列更进一步地提升了 Diff 算法的性能。