React源码分析4-深度理解diff算法

本文详细介绍了React的diff算法,包括diff策略、tree diff、component diff和element diff。通过对比新旧ReactElement,实现高效更新virtual DOM,达到O(n)的时间复杂度。diff过程分为新内容为REACT_ELEMENT_TYPE、纯文本类型和数组类型的情况,通过对每个场景的源码分析,阐述了React如何通过diff算法实现DOM的最小化更新。
摘要由CSDN通过智能技术生成

上一章中 react 的 render 阶段,其中 begin 时会调用 reconcileChildren 函数, reconcileChildren 中做的事情就是 react 知名的 diff 过程,本章会对 diff 算法进行讲解。

diff 算法介绍

react 的每次更新,都会将新的 ReactElement 内容与旧的 fiber 树作对比,比较出它们的差异后,构建新的 fiber 树,将差异点放入更新队列之中,从而对真实 dom 进行 render。简单来说就是如何通过最小代价将旧的 fiber 树转换为新的 fiber 树。

经典的 diff 算法 中,将一棵树转为另一棵树的最低时间复杂度为 O(n^3),其中 n 为树种节点的个数。假如采用这种 diff 算法,一个应用有 1000 个节点的情况下,需要比较 十亿 次才能将 dom 树更新完成,显然这个性能是无法让人接受的。

因此,想要将 diff 应用于 virtual dom 中,必须实现一种高效的 diff 算法。React 便通过制定了一套大胆的策略,实现了 O(n) 的时间复杂度更新 virtual dom。

diff 策略

react 将 diff 算法优化到 O(n) 的时间复杂度,基于了以下三个前提策略:

  • 只对同级元素进行比较。Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计,如果出现跨层级的 dom 节点更新,则不进行复用。
  • 两个不同类型的组件会产生两棵不同的树形结构。
  • 对同一层级的子节点,开发者可以通过 key 来确定哪些子元素可以在不同渲染中保持稳定。

上面的三种 diff 策略,分别对应着 tree diff、component diff 和 element diff。

tree diff

根据策略一,react 会对 fiber 树进行分层比较,只比较同级元素。这里的同级指的是同一个父节点下的子节点(往上的祖先节点也都是同一个),而不是树的深度相同。

如上图所示,react 的 tree diff 是采用深度优先遍历,所以要比较的元素向上的祖先元素都会一致,即图中会对相同颜色的方框内圈出的元素进行比较,例如左边树的 A 节点下的子节点 C、D 会与右边树 A 节点下的 C、D、E进行比较。

当元素出现跨层级的移动时,例如下图: A 子树从 root 节点下到了 B 节点下,在 react diff 过程中并不会直接将 A 子树移动到 B 子树下,而是进行如下操作:

  1. 在 root 节点下删除 A 节点
  2. 在 B 节点下创建 A 子节点
  3. 在新创建的 A 子节点下创建 C、D 节点

component diff

对于组件之间的比较,只要它们的类型不同,就判断为它们是两棵不同的树形结构,直接会将它们给替换掉。

例如下面的两棵树,左边树 B 节点和右边树 K 节点除了类型不同(比如 B 为 div 类型,K 为 p 类型),内容完全一致,但 react 依然后直接替换掉整个节点。实际经过的变换是:

  1. 在 root 节点下创建 K 节点
  2. 在 K 节点下创建 E、F 节点
  3. 在 F 节点下创建 G、H 节点
  4. 在 root 节点下删除 B 子节点

虽然如果在本例中改变类型复用子元素性能会更高一点,但是在时机应用开发中类型不一致子内容完全一致的情况极少,对这种情况过多判断反而会增加时机复杂度,降低平均性能。

element diff

react 对于同层级的元素进行比较时,会通过 key 对元素进行比较以识别哪些元素可以稳定的渲染。同级元素的比较存在插入删除移动三种操作。

如下图左边的树想要转变为右边的树:

实际经过的变换如下:

  1. 将 root 节点下 A 子节点移动至 B 子节点之后
  2. 在 root 节点下新增 E 子节点
  3. 将 root 节点下 C 子节点删除

结合源码看 diff

整体流程

diff 算法从 reconcileChildren 函数开始,根据当前 fiber 是否存在,决定是直接渲染新的 ReactElement 内容还是与当前 fiber 去进行 Diff,相关参考视频讲解:进入学习

export function reconcileChildren(
  current: Fiber | null, // 当前 fiber 节点
  workInProgress: Fiber, // 父 fiber
  nextChildren: any, // 新生成的 ReactElement 内容
  renderLanes: Lanes, // 渲染的优先级
) {
   
  if (current === null) {
   
    // 如果当前 fiber 节点为空,则直接将新的 ReactElement 内容生成新的 fiber
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
   
    // 当前 fiber 节点不为空,则与新生成的 ReactElement 内容进行 diff
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

因为我们主要是要学习 diff 算法,所以我们暂时先不关心 mountChildFibers 函数,主要关注 reconcileChildFibers ,我们来看一下它的源码:

function reconcileChildFibers(
  returnFiber: Fiber, // 父 Fiber
  currentFirstChild: Fiber | null, // 父 fiber 下要对比的第一个子 fiber
  newChild: any, // 更新后的 React.Element 内容
  lanes: Lanes, // 更新的优先级
): Fiber | null {
   

  // 对新创建的 ReactElement 最外层是 fragment 类型单独处理,比较其 children
  const isUnkeyedTopLevelFragment =
    typeof newChild === 'object' &&
    newChild !== null &&
    newChild.type === REACT_FRAGMENT_TYPE &&
    newChild.key === null;
  if (isUnkeyedTopLevelFragment) {
   
    newChild = newChild.props.children;
  }

  // 对更新后的 React.Element 是单节点的处理
  if (typeof newChild === 'object' && newChild !== null) {
   
    switch (newChild.$$typeof) {
   
      // 常规 react 元素
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      // react.portal 类型
      case REACT_PORTAL_TYPE:
        return placeSingleChild(
          reconcileSinglePortal(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );
      // react.lazy 类型
      case REACT_LAZY_TYPE:
        if (enableLazyElements) {
   
          const payload = newChild._payload;
          const init = newChild
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值