【React源码 - Diff算法】

这篇具有很好参考价值的文章主要介绍了【React源码 - Diff算法】。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

介绍

在React学习中,Diff算法(协调算法),想必我们并不陌生,简单来说就是一个对比新老节点寻找差异,然后找出最小的一个变化集,最后对这个最小变化集进行最小的DOM操作,本文将从源码来分析在React(17.0.2)中是如何来通过这个算法来进行对比并让Renderer知道如果操作DOM的。

在reconcileChildFibers中,主要是通过newChild的类型以及type来判断执行那个函数来更新fiber,其中主要类型分为非空对象、字符串/数字、数组、可迭代类型。
【React源码 - Diff算法】,react.js,算法,javascript
其中我们主要对非空对象的React Element(reconcileSingleElement函数)以及数组(reconcileChildrenArray函数,可迭代reconcileChildrenIterator和该函数核心逻辑大致一样)的处理来分析DIff算法:

if (isObject) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_LAZY_TYPE:
          if (enableLazyElements) {
            const payload = newChild._payload;
            const init = newChild._init;
            // TODO: This function is supposed to be non-recursive.
            return reconcileChildFibers(
              returnFiber,
              currentFirstChild,
              init(payload),
              lanes,
            );
          }
      }
    }
if (isArray(newChild)) {
	 return reconcileChildrenArray(
      returnFiber,
      currentFirstChild,
      newChild,
      lanes,
    );
  }

DIff算法可以分为两种:

  • 单节点对比(reconcileSingleElement)
  • 多节点对比(reconcileChildrenArray)

单节点对比

我们从reconcileSingleElement函数来分析,当虚拟DOM为一个非空对象时,React是如何进行单节点对比的:

function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  
  // 首先判断是否存在对应DOM节点
  while (child !== null) {
    // 上一次更新存在DOM节点,接下来判断是否可复用

    // 首先比较key是否相同
    if (child.key === key) {

      // key相同,接下来比较type是否相同

      switch (child.tag) {
        // ...省略case
        
        default: {
          if (child.elementType === element.type) {
          	// 将该fiber及其兄弟fiber标记为删除
             deleteRemainingChildren(returnFiber, child.sibling);
             // type相同则表示可以复用
             const existing = useFiber(child, element.props);
             // 更新复用节点的ref
             existing.ref = coerceRef(returnFiber, child, element);
             existing.return = returnFiber;
            // 返回复用的fiber
            return existing;
          }
          
          // type不同则跳出switch
          break;
        }
      }
      // 代码执行到这里代表:key相同但是type不同
      // 将该fiber及其兄弟fiber标记为删除
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      // key不同,将该fiber标记为删除
      deleteChild(returnFiber, child);
    }
    child = child.sibling;
  }

  // 创建新Fiber,并返回 ...省略
}

从代码中能看出,先判断当前fiber是否有对应的dom节点,如果是新增节点(child === null), 直接新建 fiber, 没有多余的逻辑。如果是对比更新,如果key不一样则给当前fiber打上delete的标记,继续遍历兄弟节点,key一样type不一样则给当前fiber和其兄弟节点都打上delete标记,key和type都相同(即: ReactElement.key === Fiber.key 且 Fiber.elementType === ReactElement.type), 则复用useFiber,并更新ref,否则新建。

注意: 复用过程是调用useFiber(child, element.props)创建新的fiber对象, 这个新fiber对象.stateNode = currentFirstChild.stateNode, 即stateNode属性得到了复用, 故 DOM 节点得到了复用.因为stateNode保存的就是DOM节点信息.所以常说的复用节点可以简单粗暴的理解为复用stateNode属性

当key相同且type不同时,代表我们已经根据key找到本次更新组件,但是组件发生了更新,不能复用。既key的唯一可能性已经不能复用,则剩下的fiber都没有继续查找的必要,所以都需要标记删除。
当key不同时只代表遍历到的该fiber不能被复用,后面还有兄弟fiber还没有遍历到。所以仅仅标记该fiber删除。

代码流程可以简述为:
【React源码 - Diff算法】,react.js,算法,javascript

多节点对比

这里我们通过reconcileChildrenArray来分析React中对于多节点是如何进行对比复用的。

// reconcileChildFibers函数中
if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }

通过isArray判断是否是数组,如果是则进行多节点Diff对比,会进行两次遍历,

function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
): Fiber | null {
  let resultingFirstChild: Fiber | null = null;
  let previousNewFiber: Fiber | null = null;

  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;
  // 1. 第一次循环: 遍历最长公共序列(key相同), 公共序列的节点都视为可复用
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // 后文分析
  }

  if (newIdx === newChildren.length) {
    // 如果newChildren序列被遍历完, 那么oldFiber序列中剩余节点都视为删除(打上Deletion标记)
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }

  if (oldFiber === null) {
    // 如果oldFiber序列被遍历完, 那么newChildren序列中剩余节点都视为新增(打上Placement标记)
    for (; newIdx < newChildren.length; newIdx++) {
      // 后文分析
    }
    return resultingFirstChild;
  }

  // ==================分割线==================
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  // 2. 第二次循环: 遍历剩余非公共序列, 优先复用oldFiber序列中的节点
  for (; newIdx < newChildren.length; newIdx++) {}

  if (shouldTrackSideEffects) {
    // newChildren已经遍历完, 那么oldFiber序列中剩余节点都视为删除(打上Deletion标记)
    existingChildren.forEach((child) => deleteChild(returnFiber, child));
  }

  return resultingFirstChild;
}

所谓的新老节点对比,在这里就是currentFirstChild和newChildren两个序列的对比:

  • currentFirstChild: 是一个fiber节点, 通过fiber.sibling可以将兄弟节点全部遍历出来. 所以可以将currentFirstChild理解为链表头部, 它代表一个序列, 源码中被记为oldFiber.
  • newChildren: 是一个数组, 其中包含了若干个ReactElement对象. 所以newChildren也代表一个序列.

所以reconcileChildrenArray实际就是 2 个序列之间的比较(链表oldFiber和数组newChildren), 最后返回合理的fiber序列.
上述代码中, 以注释分割线为界限, 整个核心逻辑分为 2 步骤:

  • 第一次循环: 遍历最长公共序列(key 相同), 公共序列的节点都视为可复用
    如果newChildren序列被遍历完, 那么oldFiber序列中剩余节点都视为删除(打上Deletion标记)
    如果oldFiber序列被遍历完, 那么newChildren序列中剩余节点都视为新增(打上Placement标记)
  • 第二次循环: 遍历剩余非公共序列, 优先复用 oldFiber 序列中的节点,以[key, oldFiber]的形式Map结构存储,方便快速查找复用节点
    在对比更新阶段(非初次创建fiber, 此时shouldTrackSideEffects被设置为 true). 第二次循环遍历完成之后, oldFiber序列中没有匹配上的节点都视为删除(打上Deletion标记)

假设有如下图所示 2 个初始化序列:

【React源码 - Diff算法】,react.js,算法,javascript
接下来第一次循环, 会遍历公共序列A,B, 生成的 fiber 节点fiber(A), fiber(B)可以复用.
【React源码 - Diff算法】,react.js,算法,javascript
最后第二次循环, 会遍历剩余序列E,C,X,Y:

  • 生成的 fiber 节点fiber(E), fiber©可以复用. 其中fiber©节点发生了位移(打上Placement标记).
  • fiber(X), fiber(Y)是新增(打上Placement标记).
  • 同时oldFiber序列中的fiber(D)节点确定被删除(打上Deletion标记).
    【React源码 - Diff算法】,react.js,算法,javascript
    整个主干逻辑就介绍完了, 接下来贴上完整源码

第一次循环:

// 1. 第一次循环: 遍历最长公共序列(key相同), 公共序列的节点都视为可复用
for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
  if (oldFiber.index > newIdx) {
    nextOldFiber = oldFiber;
    oldFiber = null;
  } else {
    nextOldFiber = oldFiber.sibling;
  }
  // new槽位和old槽位进行比较, 如果key不同, 返回null
  // key相同, 比较type是否一致. type一致则执行useFiber(update逻辑), type不一致则运行createXXX(insert逻辑)
  const newFiber = updateSlot(
    returnFiber,
    oldFiber,
    newChildren[newIdx],
    lanes,
  );

  if (newFiber === null) {
    // 如果返回null, 表明key不同. 无法满足公共序列条件, 退出循环
    if (oldFiber === null) {
      oldFiber = nextOldFiber;
    }
    break;
  }
  if (shouldTrackSideEffects) {
    // 若是新增节点, 则给老节点打上Deletion标记
    if (oldFiber && newFiber.alternate === null) {
      deleteChild(returnFiber, oldFiber);
    }
  }

  // lastPlacedIndex 记录被移动的节点索引
  // 如果当前节点可复用, 则要判断位置是否移动.
  lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

  // 更新resultingFirstChild结果序列
  if (previousNewFiber === null) {
    resultingFirstChild = newFiber;
  } else {
    previousNewFiber.sibling = newFiber;
  }
  previousNewFiber = newFiber;
  oldFiber = nextOldFiber;
}

第二次循环:

// 1. 将第一次循环后, oldFiber剩余序列加入到一个map中. 目的是为了第二次循环能顺利的找到可复用节点
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

// 2. 第二次循环: 遍历剩余非公共序列, 优先复用oldFiber序列中的节点
for (; newIdx < newChildren.length; newIdx++) {
  // [key, oldFiber]形式存储在Map结构中,方便快速查找
  const newFiber = updateFromMap(
    existingChildren,
    returnFiber,
    newIdx,
    newChildren[newIdx],
    lanes,
  );
  if (newFiber !== null) {
    if (shouldTrackSideEffects) {
      if (newFiber.alternate !== null) {
        // 如果newFiber是通过复用创建的, 则清理map中对应的老节点
        existingChildren.delete(newFiber.key === null ? newIdx : newFiber.key);
      }
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    // 更新resultingFirstChild结果序列
    if (previousNewFiber === null) {
      resultingFirstChild = newFiber;
    } else {
      previousNewFiber.sibling = newFiber;
    }
    previousNewFiber = newFiber;
  }
}
// 3. 善后工作, 第二次循环完成之后, existingChildren中剩余的fiber节点就是将要被删除的节点, 打上Deletion标记
if (shouldTrackSideEffects) {
  existingChildren.forEach((child) => deleteChild(returnFiber, child));
}

所以无论是单节点还是多节点、可迭代节点的比较, 最终的目的都是生成下级子节点. 并在reconcileChildren过程中, 给一些有副作用的节点(新增, 删除, 移动位置等)打上副作用标记, 等待 commit 阶段(Renderer)的处理.

时间复杂度

Diff 算法并非 React 独创,React 只是在传统 Diff 算法做了优化,将 diff 算法的时间复杂度一下子从 传统递归O(n^3)降到 两次循环O(n),其中n是树中元素数量。为了降低算法复杂度,React中对Diff算法做了以下优化:
1、同级对比
只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他,即对相同层级的 虚拟DOM 节点进行比较,同一个父节点下的所有子节点。当发现节点已经不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
【React源码 - Diff算法】,react.js,算法,javascript
如果DOM节点前后更新跨越了层级,React则不会复用,直接新建节点并删除原来节点:

【React源码 - Diff算法】,react.js,算法,javascript
2、类型比较
当比较两个节点时,首先检查它们的类型。如果类型不同,React 会销毁旧节点,并建立新节点。这种类型检查适用于不同的HTML标签或不同的组件类型。

3、Key标识
在处理动态子元素列表时,React 使用 keys 来识别每个元素的唯一性。这有助于确定哪些元素在重新渲染时保持不变,哪些元素需要更改。合理使用 keys 可以极大地提高性能,尤其是在列表和动态内容中。

4、避免不必要的 DOM 操作
通过比较新旧虚拟DOM树,React 可以确定实际需要进行的最小 DOM 更新,从而避免不必要的操作。一个虚拟DOM的Key和Type一样就认为该节点可以复用。

5、批量更新与异步渲染
React 会将多个 setState/useState 调用合并成一个批量更新,以减少渲染次数。React 17 及以后版本中,引入了新的并发模式,允许更灵活的异步渲染。

优化策略

基于React对于传递Diff的优化总结,我们可以通过以下方式来进行优化,简化对比:文章来源地址https://www.toymoban.com/news/detail-792883.html

  • Keys:在列表中使用唯一的key可以帮助React识别哪些元素改变了,哪些没有。这样React可以只重新渲染那些改变了的元素,而不是整个列表。
  • 组件级别的比较:React在比较组件时,如果组件类型相同,则会比较其props和state;如果组件类型不同,则会销毁旧组件并创建新组件。
  • 避免不必要的重新渲染:使用shouldComponentUpdate、React.memo或PureComponent来避免不必要的渲染。
  • 虚拟DOM树的结构优化:合理的组织组件结构,避免过深的虚拟DOM树,可以减少diff算法需要比较的节点数,从而提高性能。
  • 使用不可变数据:这可以帮助快速比较props和state的变化,因为不可变数据在发生变化时会产生一个新的对象。
  • 懒加载组件:对于大型应用中的一些不常用组件,可以使用懒加载技术,只有当这些组件需要被展示时,才加载它们。

到了这里,关于【React源码 - Diff算法】的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【react】react18的学习(十一)– 底层原理(一)之 diff 算法

    diff算法、fiber链表 步骤:(追求多复用,快渲染) 首次渲染,缓存虚拟dom或fiber链表(17及以后); 组件更新,将新生成的虚拟dom与已有的真实dom的fiber链表对比; 遵循同级对比、深度对比原则,先依次找节点对比; 对比过程中,第一轮对比:按链表顺序对比,节点key值相

    2024年02月17日
    浏览(40)
  • 后端程序员入门react笔记——react的diff算法(三)

    我们知道,react里面操作的都是虚拟dom,最后经过render渲染为真正的dom,那么为什么要提出虚拟dom这个概念呢?其实就是将逻辑和视图区分开,react的虚拟dom,就相当于mvc的c,将数据逻辑和真正的dom区分开,我们知道,对于前端来说dom操作是非常昂贵的,性能消耗最大的就是

    2024年02月22日
    浏览(47)
  • JavaScript 框架比较:Angular、React、Vue.js

    在 Web 开发领域,JavaScript 提供大量技术栈可供选择。其中最典型的三套组合,分别是 MERN、MEAN 和 MEVN。这些首字母相同的选项各自代表不同的技术加工具组合。为了在这些技术栈中做出明智选择,让我们先从核心组件聊起,再对各自前端框架(React、Angular 和 Vue)进行简化比

    2024年01月20日
    浏览(59)
  • JavaScript框架 Angular、React、Vue.js 的全栈解决方案比较

    在 Web 开发领域,JavaScript 提供大量技术栈可供选择。其中最典型的三套组合,分别是 MERN、MEAN 和 MEVN。前端框架(React、Angular 和 Vue)进行简化比较。 MERN 技术栈包含四大具体组件: MongoDB:一款强大的 NoSQL 数据库,以灵活的 JSON 格式存储数据。 Express.js:一套极简但强大的

    2024年02月03日
    浏览(58)
  • 2023年最佳JavaScript框架:React、Vue、Angular和Node.js的比较

    🎉欢迎来到Java学习路线专栏~探索2023年最佳JavaScript框架:React、Vue、Angular和Node.js的比较 ☆* o(≧▽≦)o *☆嗨~我是IT·陈寒🍹 ✨博客主页:IT·陈寒的博客 🎈该系列文章专栏:Java学习路线 📜其他专栏:Java学习路线 Java面试技巧 Java实战项目 AIGC人工智能 🍹文章作者技术和水

    2024年02月11日
    浏览(49)
  • Node.js npm V8 React Express的运行配合关系:构建JavaScript应用的基石

    目录 Node.js 和 V8 引擎 Node.js 和 npm LTS(Long Term Support) React Node.js的作用 Express Node.js 和 V8 引擎 Node.js 使用 Google 的 V8 JavaScript 引擎 来执行 JavaScript 代码。V8 是一个高性能的 JavaScript 和 WebAssembly 引擎,用于在 Google Chrome 浏览器和 Node.js 中运行 JavaScript。 V8 引擎的更新 通常包括

    2024年03月12日
    浏览(64)
  • react+vue 前端国密算法sm2、sm3 、sm4的js ts实现

    1. 简单介绍下SM2 和 SM3 SM2 算法:是一种公钥加密算法,它的密钥长度为 256 位,安全性较高。可用于数字签名、密钥协商等场景。 SM3 算法:是一种对称加密算法,用于消息摘要和数字签名等场景。它的密钥长度为 256 位,安全性较高。SM3 算法与 SM2 算法相互配合,提高了整体

    2024年01月19日
    浏览(41)
  • JavaScript之React

    前言 React 是一个用于构建用户界面的 JavaScript 库,由 Facebook 开发。它可以让开发者编写可重复使用的 UI 组件,并且可以自动地更新 UI。React 的运行原理可以分为两个部分:虚拟 DOM 和组件更新。 虚拟 DOM React 使用虚拟 DOM 来代表实际的 DOM 树。虚拟 DOM 是一个轻量级的 JavaSc

    2024年02月06日
    浏览(43)
  • 章节 1:入门React.js -《React.js手把手教程:从初学者到实战高手》- 第一部分:React.js基础

    《React.js手把手教程:从初学者到实战高手》 第一部分:React.js基础 在我们开始探索 React.js 的奇妙世界之前,让我们先来认识一下这个“ 人人都在谈论 ”的家伙。你可以把 React.js 想象成是一个全栈工程师的一大碗甜品,不仅好吃,还可以让你的应用更美味!那么,究竟什么

    2024年02月11日
    浏览(53)
  • React框架:TypeScript支持的JavaScript库

    React 框架是一个功能强大的 JavaScript 库,让用户可以轻松地构建高度动态的用户界面。它借助虚拟 DOM 的思想实现高效的性能,并具有易于使用和灵活的编程接口。随着越来越多的人开始使用 React ,在不断的发展和变化中, React 框架现在加入了 TypeScript 的支持,使其成为一个

    2024年02月11日
    浏览(66)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包