热门标签 | HotTags
当前位置:  开发笔记 > 人工智能 > 正文

reactdiff算法源码解析

这篇文章主要介绍了reactdiff算法源码解析的相关资料,帮助大家更好的理解和学习使用react,感兴趣的朋友可以了解下

React中Diff算法又称为调和算法,对应函数名为reconcileChildren,它的主要作用是标记更新过程中那些元素发生了变化,这些变化包括新增、移动、删除。过程发生在beginWork阶段,只有非初次渲染才会Diff。

以前看过一些文章将Diff算法表述为两颗Fiber树的比较,这是不正确的,实际的Diff过程是一组现有的Fiber节点和新的由JSX生成的ReactElement的比较,然后生成新的Fiber节点的过程,这个过程中也会尝试复用现有Fiber节点。

节点Diff又分为两种:

  1. 单节点Diff —— ElementPortalstringnumber
  2. 多节点Diff —— ArrayIterator

以下React版本为17.0.1,代码文件为ReactChildFiber.old.js。

单节点Diff

单节点Diff比较简单,只有key相同并且type相同的情况才会尝试复用节点,否则会返回新的节点。

单节点大部分情况下我们都不会去赋值key,所以它们默认为null,也是相同的。

reconcileSingleElement

  // 单节点比较
  function reconcileSingleElement(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    // 当前新的reactElement的key
    const key = element.key;
    // 当前的child fiber节点
    let child = currentFirstChild;
    while (child !== null) {
      // key相同的情况才diff
      if (child.key === key) {
        switch (child.tag) {
          // ...
          default: {
            // 当前fiber和reactElement的type相同时
            if (child.elementType === element.type) {
              // 删除同级的其他节点
              deleteRemainingChildren(returnFiber, child.sibling);
              // 复用当前child fiber
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber;
              // 返回可复用的child fiber
              return existing;
            }
            break;
          }
        }
        // 不匹配删除节点
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        // key不同直接删除节点
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    // 新的Fiber节点
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
  }

多节点Diff

源码中将多节点分为了数组节点和可迭代节点。

if (isArray(newChild)) {
  return reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
  );
}

if (getIteratorFn(newChild)) {
  return reconcileChildrenIterator(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
  );
}

对应的Diff函数分别是reconcileChildrenArrayreconcileChildrenIterator。它们的核心Diff逻辑是相同的,所以只分析数组节点的Diff —— reconcileChildrenArray函数。

这一段的代码比较长,但逻辑很清晰,从分割线分为两轮遍历。

  • 第一轮遍历的是顺序相同且key也相同的节点,这些节点需要做更新操作。
  • 第二轮遍历的是顺序不同,可能key也不同的节点,这些节点需要做新增、移动或删除操作。

第一轮遍历只针对key和顺序都相同的情况,这些key对应的节点位置没有发生改变,只需要做更新操作,一旦遍历遇到key不同的情况就需要跳出循环。

// 旧节点
  • // 新节点
  • // key="5"不同,跳出遍历 // 第一轮遍历的节点
  • //
  • 留在第二轮遍历比较。
  • 在第一轮遍历完后也分为两种情况。

    1. 新节点数量少于旧节点数量,这时候需要把多余的旧节点标记为删除。
    2. 新节点数量大于旧节点数量,这时候需要把多余的新节点标记为新增。

    第二轮遍历针对key不同或顺序不同的情况,可能情况如下:

    // 旧节点
    
  • // 新节点
  • // 第二轮遍历对比
  • 这两个节点
  • 第二轮的遍历会稍微复杂一点,后文在细讲。

    详细的代码如下。

    reconcileChildrenArray

      function reconcileChildrenArray(
        returnFiber: Fiber,
        currentFirstChild: Fiber | null,
        newChildren: Array<*>,
        lanes: Lanes,
      ): Fiber | null {
        // 函数返回的Fiber节点
        let resultingFirstChild: Fiber | null = null;
        let previousNewFiber: Fiber | null = null;
    
        // oldFiber为链表
        let oldFiber = currentFirstChild;
        // 用来判断节点是否移动
        let lastPlacedIndex = 0;
        let newIdx = 0;
        let nextOldFiber = null;
        // 第一轮遍历,只遍历key相同的节点
        for (; oldFiber !== null && newIdx  newIdx) {
            nextOldFiber = oldFiber;
            oldFiber = null;
          } else {
            // 每次循环旧的fiber节点都会指向兄弟元素也就是下次循环的fiber节点
            nextOldFiber = oldFiber.sibling;
          }
          // key相同返回fiber节点,key不同返回null
          // 如果type相同复用节点,不同返回新节点
          const newFiber = updateSlot(
            returnFiber,
            oldFiber,
            newChildren[newIdx],
            lanes,
          );
          // newFiber为null表示key不同,跳出循环
          if (newFiber === null) {
            if (oldFiber === null) {
              oldFiber = nextOldFiber;
            }
            break;
          }
          // newFiber.alternate为null就是新节点,说明type不同创建了新fiber节点
          if (oldFiber && newFiber.alternate === null) {
            // 需要把oldFiber标记删除
            deleteChild(returnFiber, oldFiber);
          }
          // 放置节点,更新lastPlacedIndex
          lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
          // 组成新fiber节点链
          if (previousNewFiber === null) {
            resultingFirstChild = newFiber;
          } else {
            previousNewFiber.sibling = newFiber;
          }
          previousNewFiber = newFiber;
          oldFiber = nextOldFiber;
        }
    
        /*
        第一轮遍历完后新节点数量少于旧节点数量
        newChildren已经遍历完,删除掉剩下的fiber节点,可能情况如下 &#11015;&#65039;
        以前
        
  • 新的
  • 就会把
  • 删除 */ if (newIdx === newChildren.length) { deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } /* 第一轮遍历完新节点数量大于旧节点数量 oldFiber已经遍历完,可能情况如下 &#11015;&#65039; 以前
  • 新的
  • 就会添加新的
  • ,这一段是新节点的插入逻辑 */ if (oldFiber === null) { for (; newIdx fiber节点的Map,方便用key来获取对应的旧fiber节点 const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // 第二轮遍历,继续遍历剩余的节点,这些节点可能是需要移动或者删除的 for (; newIdx deleteChild(returnFiber, child)); return resultingFirstChild; }
  • 第一轮遍历比较好理解,这里再细分析一下第二轮遍历,因为第二轮会出现复用是否需要移动的问题。

    第二轮遍历首先遍历剩余的oldFiber,组成一个key -> 旧fiber节点的Map,这用可以通过key来快速的获取旧节点。

    接下来的遍历依然是使用的新节点为遍历对象,每次遍历使用新节点的key从Map中取出旧节点来对比是否能复用,对应的函数为updateFromMap

    如果节点存在alternate属性,则是复用的节点,这时候需要将它从existingChildren里移除,后续会把第二轮遍历完后依然存在在existingChildren里的节点标记为删除。

    如何判断节点移动了?

    这里存在一个变量lastPlacedIndex用来判断节点是否移动,每次将节点添加到新的Fiber链表中,都会更新这个值。

    当复用的节点oldIndex小于lastPlacedIndex时,则为移动,如果不需要移动,则会将lastPlacedIndex更新为较大的oldIndex,下一个节点会以新值判断,代码如下:

    function placeChild(
      newFiber: Fiber,
      lastPlacedIndex: number,
      newIndex: number,
    ): number {
      newFiber.index = newIndex;
      const current = newFiber.alternate;
      if (current !== null) {
        const oldIndex = current.index;
        if (oldIndex 
    

    举个例子:

    // 旧
    abcd
    // 新
    acbd
    

    abcd均为key值。

    第一轮遍历后剩下的需要对比节点:

    // 旧
    bcd
    // 新
    cbd
    

    a节点在第一轮已经复用,并且调用过placeChild,这时lastPlacedIndex值为0。

    进入第二轮遍历,依然是以新节点为遍历对象。

    c => 在旧节点中存在,可复用,它的index在旧节点中为2,2 > lastPlacedIndex(0),不需要移动,将lastPlacedIndex赋值为2。
    b => 在旧节点中存在,可复用,它的index在旧节点中为1,1  在旧节点中存在,可复用,它的index在旧节点中为3,3 > lastPlacedIndex(2),不需要移动。
    

    由这个例子可以看出,React中将右侧不需要移动的节点作为参照,将需要移动的节点都是统一从左向右移动的。

    在后续Layout阶段会将这里标记了Placement的节点做insertBefore操作。

    总结

    React中的Diff算法核心代码不算很长,但是却引入key巧妙的将复杂度由O(n3 )变为了O(n)。

    码农内卷太严重,所以不得不学习源码了。

    以上就是react diff算法源码解析的详细内容,更多关于react diff算法的资料请关注其它相关文章!


    推荐阅读
    • Søren Kierkegaard famously stated that life can only be understood in retrospect but must be lived moving forward. This perspective delves into the intricate relationship between our lived experiences and our reflections on them. ... [详细]
    • 计算机网络复习:第五章 网络层控制平面
      本文探讨了网络层的控制平面,包括转发和路由选择的基本原理。转发在数据平面上实现,通过配置路由器中的转发表完成;而路由选择则在控制平面上进行,涉及路由器中路由表的配置与更新。此外,文章还介绍了ICMP协议、两种控制平面的实现方法、路由选择算法及其分类等内容。 ... [详细]
    • 本文介绍了Java并发库中的阻塞队列(BlockingQueue)及其典型应用场景。通过具体实例,展示了如何利用LinkedBlockingQueue实现线程间高效、安全的数据传递,并结合线程池和原子类优化性能。 ... [详细]
    • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
    • 深入理解C++中的KMP算法:高效字符串匹配的利器
      本文详细介绍C++中实现KMP算法的方法,探讨其在字符串匹配问题上的优势。通过对比暴力匹配(BF)算法,展示KMP算法如何利用前缀表优化匹配过程,显著提升效率。 ... [详细]
    • 探讨一个显示数字的故障计算器,它支持两种操作:将当前数字乘以2或减去1。本文将详细介绍如何用最少的操作次数将初始值X转换为目标值Y。 ... [详细]
    • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
    • 本文探讨如何设计一个安全的加密和验证算法,确保生成的密码具有高随机性和低重复率,并提供相应的验证机制。 ... [详细]
    • 深入解析:手把手教你构建决策树算法
      本文详细介绍了机器学习中广泛应用的决策树算法,通过天气数据集的实例演示了ID3和CART算法的手动推导过程。文章长度约2000字,建议阅读时间5分钟。 ... [详细]
    • 在金融和会计领域,准确无误地填写票据和结算凭证至关重要。这些文件不仅是支付结算和现金收付的重要依据,还直接关系到交易的安全性和准确性。本文介绍了一种使用C语言实现小写金额转换为大写金额的方法,确保数据的标准化和规范化。 ... [详细]
    • 在给定的数组中,除了一个数字外,其他所有数字都是相同的。任务是找到这个唯一的不同数字。例如,findUniq([1, 1, 1, 2, 1, 1]) 返回 2,findUniq([0, 0, 0.55, 0, 0]) 返回 0.55。 ... [详细]
    • 本文探讨了卷积神经网络(CNN)中感受野的概念及其与锚框(anchor box)的关系。感受野定义了特征图上每个像素点对应的输入图像区域大小,而锚框则是在每个像素中心生成的多个不同尺寸和宽高比的边界框。两者在目标检测任务中起到关键作用。 ... [详细]
    • 网络攻防实战:从HTTP到HTTPS的演变
      本文通过一系列日记记录了从发现漏洞到逐步加强安全措施的过程,探讨了如何应对网络攻击并最终实现全面的安全防护。 ... [详细]
    • 本文深入探讨了Linux系统中网卡绑定(bonding)的七种工作模式。网卡绑定技术通过将多个物理网卡组合成一个逻辑网卡,实现网络冗余、带宽聚合和负载均衡,在生产环境中广泛应用。文章详细介绍了每种模式的特点、适用场景及配置方法。 ... [详细]
    • 本文探讨了如何在给定整数N的情况下,找到两个不同的整数a和b,使得它们的和最大,并且满足特定的数学条件。 ... [详细]
    author-avatar
    mobiledu2502889253
    这个家伙很懒,什么也没留下!
    PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
    Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有