从源码层面理解 React 是如何做 diff 的

news2024/9/23 17:14:28

大家好,我是前端西瓜哥。今天带带大家来分析 React 源码,理解它的单节点 diff 和多节点 diff 的具体实现。

React 的版本为 18.2.0

reconcileChildFibers

React 的节点对比逻辑是在 reconcileChildFibers 方法中实现的。

reconcileChildFibers 是 ChildReconciler 方法内部定义的方法,通过调用 ChildReconciler 方法,并传入一个 shouldTrackSideEffects 参数返回。这样做是为了根据不同使用场景 ,产生不同的效果。

因为一个组件的更新和挂载的流程不同的。比如挂载会执行挂载的生命周期函数,更新则不会。

// reconcileChildFibers,和内部方法同名
export const reconcileChildFibers = ChildReconciler(true);

// mountChildFibers 是在一个节点从无到有的情况下调用
export const mountChildFibers = ChildReconciler(false);

reconcileChildFibers 的核心实现:

function reconcileChildFibers(
  returnFiber,
  currentFirstChild,
  newChild,
  lanes,
) {
  // newChild 可能是数组或对象
  // 如果是数组,那它的 $$typeof 就是 undefined
  switch (newChild.$$typeof) {
    case REACT_ELEMENT_TYPE:
      // 单节点 diff
      return placeSingleChild(
        reconcileSingleElement(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        ),
      );
    // ...
  }
  
  // 多节点 diff
  if (isArray(newChild)) {
    return reconcileChildrenArray(
      returnFiber,
      currentFirstChild,
      newChild,
      lanes,
    );
  }
}

newChild 是在组件 render 时得到 ReactElement,通过访问组件的 props.children 得到。

如果 newChild 是对象(非数组),会 调用 reconcileSingleElement(普通元素的情况),做单个节点的对比。

如果是数组时,就会 调用 reconcileChildrenArray,进行多节点的 diff。

更新和挂载的逻辑有点不同,后面都会用 “更新” 的场景进行讲解。

单节点 diff

先看看 单节点 diff

需要注意的是,这里的 “单节点” 指的是新生成的 ReactElement 是单个的。只要新节点是数组就不算单节点,即使数组长度只为 1。 此外旧节点可能是有兄弟节点的(sibling 不为 null)。

fiber 对象是通过链表来表示节点之间的关系的,它的 sibling 指向它的下一个兄弟节点,index 表示在兄弟节点中的位置。

ReactElement 则是对象或数组的形式,通过 React.createElement() 生成。

单节点 diff 对应 reconcileSingleElement 方法,其核心实现为:

function reconcileSingleElement(
  returnFiber, // 父 fiber
  currentFirstChild, // 更新前的 fiber
  element, // 新的 ReactElement
) {
  const key = element.key;
  let child = currentFirstChild;

  while (child !== null) {
 
    if (child.key === key) {
      const elementType = element.type;
      // key 相同,且类型相同(比如新旧都是 div 类型)
      // 则走 “更新” 逻辑
      if (child.elementType === elementType) {
        // 【分支 1】
        // 将旧节点后所有的 sibling 打上删除 tag
        deleteRemainingChildren(returnFiber, child.sibling);

        // 创建 WorkInProgress,也就是原来 fiber 的替身啦
        const existing = useFiber(child, element.props.children);
        existing.return = returnFiber;
        return existing;
      } else {
        //【分支 2】
        deleteRemainingChildren(returnFiber, child);
        break;
      }
    } 
    // 当前节点 key 不匹配,将它标记为待删除
    else {
      // 【分支 3】
      deleteChild(returnFiber, child);
    }
    // 取下一个兄弟节点,继续做对比
    child = child.sibling;
  }
    
  // 执行到这里说明没发现可复用节点,需要创建一个 fiber 出来
  const created = createFiberFromElement(element, returnFiber.mode, lanes);
  created.return = returnFiber;
  return created;
}

currentFirstChild 是更新前的节点,它是以链表的保存的,它的 sibling 指向它的下一个兄弟节点。

分支很多,下面我们进行详细地分析。

分支 1:key 相同且 type 相同

当发现 key 相同时,React 会尝试复用组件。新旧节点的 key 都没有设置的话,会设置为 null,如果新旧节点的 key 都为 null,会认为相等。

此外还要判断新旧类型是否相同(比如都是 div),因为类型都不同了,是无法复用的。

如果都满足,就会将旧 fiber 的后面的兄弟节点都标记为待删除,具体是调用 deleteRemainingChildren() 方法,它会在父 fiber 的 deletions 数组上,添加指定的子 fiber 和它之后的所有兄弟节点,作为删除标记。

之后的 commit 阶段会再进行正式的删除,再执行一些调用生命周期函数等逻辑。

useFiber() 会创建旧的 fiber 的替身,更新到 fiber 的 alternate 属性上,最后这个 useFiber 返回这个 alternate。然后直接 return,结束这个方法。

分支 2:key 相同但 type 不同

type 不同是无法复用的,如果 type 不同但 key 却相同,React 会认为没有匹配的可复用节点了。直接就将剩下的兄弟节点标记为删除,然后结束循环。

分支 3:key 不匹配

key 不同,用 deleteChild() 方法将当前的 fiber 节点标记为待删除,取出下一个兄弟节点再和新节点再比较,不断循环,直到匹配到其中一种分支为止。

以上就是三个分支。

如果能走到循环结束,说明没能找到能复用的 fiber,就会根据 ReactElement 调用 createFiberFromElement() 方法创建一个新的 fiber,然后返回它。

外部会拿到这个 fiber,调用 placeSingleChild() 将其 打上待更新 tag。

reconcileChildrenArray

然后是 多节点 diff

对应 ReactElement 为数组的场景,这种场景的算法实现要复杂的多。

多节点 diff 对应 reconcileChildrenArray 方法,因为算法比较复杂,先不直接贴比较完整的代码,而是分成几个阶段去一点点讲解。

多节点的 diff 分 4 个阶段,下面细说。

阶段1:同时从左往右遍历

image-20221206224504796

旧 fiber 和 element 各自的指针一起从左往右走。指针分别为 nextFiber 和 newIdx,从左往右不断遍历。

遍历中发生的逻辑有:

  1. 有一个指针走完,即 nextFiber 变成 null 或 newIdx 大于 newChildren.length,循环结束;
  2. 如果 key 不同,就会结束遍历(在源码中的体现是 updateSlot() 返回 null 赋值给 newFiber,然后就 break 跳出循环);
  3. 如果 key 相同,但 type 不同,说明这个旧节点是不能用的了,给它 打上 “删除” 标记,然后继续遍历;
  4. key 相同,type 也相同,复用节点。对于普通元素类型,最终会调用 updateElement 方法。

updateElement 方法会判断 fiber 和 element 的类型是否相同,如果相同,会给 fiber 的 alternate 生成一个 workInProcess(替身) fiber 返回,否则 创建一个新的 fiber 返回。它们会带上新的 pendingProps 属性。

function reconcileChildrenArray(
  returnFiber,
  currentFirstChild, // 旧的 fiber
  newChildren, // 新节点数组
  lanes,
) {
  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;
 
  // 【1】分别从左往右遍历对比更新
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    if (oldFiber.index > newIdx) { // 旧 fiber 比新 element 多
      nextOldFiber = oldFiber;
      oldFiber = null;
    } else {
      nextOldFiber = oldFiber.sibling;
    }
    // 更新节点(或生成新的待插入节点)
    // 方法内部会判断 key 是否相等,不相等会返回 null。
    const newFiber = updateSlot(
      returnFiber,
      oldFiber,
      newChildren[newIdx],
      lanes,
    );

    // 如果当前新旧节点不匹配,就跳出循环
    if (newFiber === null) {
      if (oldFiber === null) {
        oldFiber = nextOldFiber;
      }
      break;
    }

    if (shouldTrackSideEffects) {
      if (oldFiber && newFiber.alternate === null) {
        // newFiber 不是基于 oldFiber 的 alternate 创建的
        // 说明 oldFiber 要销毁掉,要打上 “删除” 标记
        deleteChild(returnFiber, oldFiber);
      }
    }
    
    // 打 “place” 标记
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
  }
}

阶段 2:新节点遍历完的情况

跳出循环后,我们先看 新节点数组是否遍历完(newIdx 是否等于 newChildren.length)。

是的话,就将旧节点中剩余的所有节点编辑为 “删除”,然后直接结束整个函数。

function reconcileChildrenArray(
  returnFiber,
  currentFirstChild, // 旧的 fiber
  newChildren, // 新节点数组
  lanes,
) {
  // 【1】分别从左往右遍历对比更新
  // ...
    
  // 【2】如果新节点遍历完,将旧节点剩余节点全都标记为删除
  if (newIdx === newChildren.length) {
    // We've reached the end of the new children. We can delete the rest.
    deleteRemainingChildren(returnFiber, oldFiber);
    return resultingFirstChild;
  }
}

阶段三:旧节点遍历完,新节点没遍历完的情况

如果是旧节点遍历完了,但新节点没有遍历完,就将新节点中的剩余节点,根据 element 构建为 fiber。

function reconcileChildrenArray(
  returnFiber,
  currentFirstChild, // 旧的 fiber
  newChildren, // 新节点数组
  lanes,
) {
  // 【1】分别从左往右遍历对比更新
  // ...
    
  // 【2】如果新节点遍历完,将旧节点剩余节点全都标记为删除
  // ...
  
  // 【3】如果旧节点遍历完了,但新节点没有遍历完,根据剩余新节点生成新 fiber
  if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      // 通过 element 创建 fiber
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      // fiber 设置 index,并打上 “placement” 标签
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        // 新建的 fiber 彼此连起来
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
    // 返回新建 fiber 中的第一个
    return resultingFirstChild;
  }
}

阶段 4:使用 map 高效匹配新旧节点进行更新

【4】如果新旧节点都没遍历完,那我们会调用 mapRemainingChildren 方法,先将剩余的旧节点,放到 Map 映射中,以便快速访问。

map 中会优先使用 fiber.key(保证会转换为字符串)作为键;如果 fiber.key 是 null,则使用 fiber.index(数值类型),key 和 index 的值是不会冲突的。值自然就是 fiber 对象本身。

然后就是遍历剩余的新节点,调用 updateFromMap 方法,从映射表中找到对应的旧节点,和新节点进行对比更新。

遍历完后就是收尾工作了,map 中剩下的就是没能匹配的旧节点,给它们打上 “删除” 标记。

function reconcileChildrenArray(
  returnFiber,
  currentFirstChild, // 旧的 fiber
  newChildren, // 新节点数组
  lanes,
) {
  // 【1】分别从左往右遍历对比更新
  // ...
    
  // 【2】如果新节点遍历完,将旧节点剩余节点全都标记为删除
  // ...
  
  // 【3】如果旧节点遍历完了,但新节点没有遍历完,根据剩余新节点生成新 fiber
  // ...

  // 【4】剩余旧节点放入 map 中,再遍历快速访问,快速进行新旧节点匹配更新。
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

  for (; newIdx < newChildren.length; newIdx++) {
    const newFiber = updateFromMap(
      existingChildren,
      returnFiber,
      newIdx,
      newChildren[newIdx],
      lanes,
    );
    if (newFiber !== null) {
      if (shouldTrackSideEffects) {
         // 是在旧 fiber 上的复用更新,所以需要移除 set 中的对应键
        if (newFiber.alternate !== null) {
          existingChildren.delete(
            newFiber.key === null ? newIdx : newFiber.key,
          );
        }
      }
      // 给 newFiber 打上 “place” 标记
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);

      // 给新 fiber 构建成链表
      // 并维持 resultingFirstChild 指向新生成节点的头个节点
      if (previousNewFiber === null) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
    }
  }
    
  // 收尾工作,将没能匹配的旧节点打上 “删除” 标记
  if (shouldTrackSideEffects) {
    // Any existing children that weren't consumed above were deleted. We need
    // to add them to the deletion list.
    existingChildren.forEach(child => deleteChild(returnFiber, child));
  }
  return resultingFirstChild;
}

结尾

有点复杂的。

我是前端西瓜哥,欢迎关注我,学习更多前端知识。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/68959.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ATTCK-T1003-001-操作系统凭据转储:LSASS内存

0x01基础信息 具体信息详情ATT&CK编号T1003-001所属战术阶段凭据访问操作系统windows 7 旗舰版 SP1创建时间2022年11月17日监测平台火绒安全、火绒剑、sysmon 0x02技术原理 攻击者可能会尝试访问存储在本地安全机构子系统服务 (LSASS) 进程内存中的凭证材料。用户登录后&…

Linux文件服务配置FTP服务

作者简介&#xff1a;一名99年软件运维应届毕业生&#xff0c;正在自学云计算课程。宣言&#xff1a;人生就是B&#xff08;birth&#xff09;和D&#xff08;death&#xff09;之间的C&#xff08;choise&#xff09;&#xff0c;做好每一个选择。创作不易&#xff0c;动动小手…

【Python恶搞】Python实现祝福单身狗的恶搞项目,快@你的好朋友,祝福他吧 | 附源码

前言 halo&#xff0c;包子们上午好 咱就说&#xff0c;谁还没有一个单身的小伙伴呢 今天这个代码主要是为了祝福咱们单身的小伙伴 咱就说废话不多说&#xff0c;直接上才艺 相关文件 关注小编&#xff0c;私信小编领取哟&#xff01; 当然别忘了一件三连哟~~ 源码点击蓝色…

数据可视化是让信息表现更复杂?很多人可能错了

数据可视化&#xff0c;目前行业中很多人认识有些偏颇&#xff0c;数据可视化就是单纯认为是大屏展示、酷炫的图表&#xff0c;很多人仅仅是把数据可视化 作为展厅中的刚性需求而已&#xff0c;其实这个是对数据行业的偏见&#xff0c;很多人侧重于数据的表现&#xff0c;而非便…

谷歌北大扩散模型(Diffusion Model)首篇综述来了!

本综述&#xff08;Diffusion Models: A Comprehensive Survey of Methods and Applications&#xff09;来自加州大学&Google Research的Ming-Hsuan Yang、北京大学崔斌实验室以及CMU、UCLA、蒙特利尔Mila研究院等众研究团队&#xff0c;首次对现有的扩散生成模型&#xf…

ftp工具的21端口无法连上远程主机

一、检测是否有安装vsffpd netstat -tunlp 没有安装先安装 1.安装 vsftpd 执行以下命令&#xff0c;安装 vsftpd。 yum install vsftpd -y 2.启动服务 执行以下命令&#xff0c;启动服务。 systemctl start vsftpd 3.执行以下命令&#xff0c;确认服务是否启动。 netstat -tun…

【c++基础】第二章 微观部分:面向对象之类的组成

第二章 微观部分&#xff1a;面向对象之类的组成类函数构造函数析构函数拷贝构造函数运算符重载函数封装一个字符串类初始化列表this指针常对象和常成员函数&#xff08;方法&#xff09;静态属性和静态成员函数单例设计模式类 对象&#xff1a;属性和方法组成&#xff0c;是类…

Nature子刊 | 空间转录组技术及其发展方向

2022年10月《Nature Biotechnology》发表了一篇空间转录组&#xff08;ST&#xff09;技术的综述文章&#xff0c;详细描述了现有的ST技术及其发展方向。 检测生物分子的新技术一直是生物进步的关键驱动力。在检测生物分子时&#xff0c;研究人员在选择实验方法时一直面临着关键…

CENTOS上的网络安全工具(十四)搬到Docker上(2)?

既然说要搬到Docker上&#xff0c;那么肯定是要把咱日常习惯用的那些东西都往docker上堆一堆看的。我最先考虑的居然是SSH&#xff0c;乃至于到现在我都不知道我为什么第一个想到的是SSH——因为对虚拟机来说&#xff0c;首先考虑的当然是如何远程管理集群中的每个机器&#xf…

iptables用法总结

iptables 是集成在 Linux 内核中的包过滤防火墙系统。使用 iptables 可以添加、删除具体的过滤规则&#xff0c;iptables 默认维护着 4 个表和 5 个链&#xff0c;所有的防火墙策略规则都被分别写入这些表与链中。 1、iptables语法格式 iptables 命令的基本语法格式如下&…

C++ 基础笔记(入门到循环)

目录 1.认识C —— 初窥门径 一、C程序框架&#xff08;8分&#xff09; 二、语言规范&#xff08;16分&#xff09; 三、DEV-C软件&#xff08;下载链接&#xff09; 四、计算机快捷键 五、输入输出的应用 2.变量与赋值 —— 脚踏实地 2.1 变量类型 2.2 变量类型与变…

一文详解名字分类(字符级RNN)

目录 一.前言 二.数据预处理 三.构造神经网络 四.训练 五.评价结果&#xff08;预测&#xff09; 一.前言 我们将构建和训练字符级RNN来对单词进行分类。字符级RNN将单词作为一系列字符读取&#xff0c;在每一步输出预测和“隐藏状态”&#xff0c;将其先前的隐藏 状态输…

02-MySQL数据管理

目录 DDL&#xff08;数据操作语言&#xff09; 添加数据 添加student表数据 修改数据 WHERE条件子句 修改student表数据 删除数据 删除student表数据 总结&#xff1a; DDL&#xff08;数据操作语言&#xff09; 用于操作数据库对象中所包含的数据 关键字&#xff1…

STM32的光敏检测自动智能窗帘控制系统proteus设计

STM32的光敏检测自动智能窗帘控制系统proteus设计 ( proteus仿真程序演示视频&#xff09; 仿真图proteus 8.9 程序编译器&#xff1a;keil 5 编程语言&#xff1a;C语言 设计编号&#xff1a;C0074 主要功能&#xff1a; 结合实际情况&#xff0c;基于STM32单片机设计一…

ATTCK-T1078-001-默认账户

0x01基础信息 具体信息详情ATT&CK编号T1078-001所属战术阶段初始访问操作系统windows 7 旗舰版 SP1创建时间2022年11月10日监测平台火绒安全、火绒剑、sysmon 0x02技术原理 攻击者可能会获取和滥用默认帐户的凭据&#xff0c;以作为获得初始访问、持久性、特权升级或防御…

Python用一行代码,截取图片

前言 本文是该专栏的第3篇,后面会持续分享python的各种黑科技知识,值得关注。 在工作上,有些时候可能需要用到代码来进行自动截图,比如说需要图文识别,进行数据信息抽取的时候,能自动定位截图无疑是很好的办法。 对python而言,截图方法其实有很多,但笔者下面要介绍的…

活动明天见 | DataFunSummit 2022 AI基础软件架构峰会圆桌会

11月16日晚 19&#xff1a;30-21:00&#xff0c;第四范式技术副总裁、OpenMLDB 项目发起人郑曌受邀主持DataFunSummit 2022 AI基础软件架构峰会圆桌会&#xff0c;将与各位资深专家在线上做深度的交流分享&#xff0c;欢迎大家届时收看。 开源机器学习数据库 OpenMLDB **活动时…

全国产三防加固计算机

国产三防加固计算机&#xff0c;本文指的是CPU为国产飞腾D2000处理器、操作系统为国产麒麟V10 SP1系统&#xff0c;整机方案采用全国产化设计。 达梦数据库是由武汉达梦数据库股份有限公司推出的具有完全自主知识产权的高性能数据库管理系统&#xff0c;简称DM。达梦数据库管理…

“辣条一哥”卫龙冲击港股IPO,我又吃出一家上市公司

一、公司简介 卫龙作为国内领先的辣味休闲食品企业&#xff0c;是目前国内辣味食品当之无愧的第一品牌。据2021年零售销售额计&#xff0c;公司在辣味休闲食品企业中排名第一&#xff0c;市场份额达到了6.2%&#xff0c;是一款备受年轻消费者喜爱的休闲食品品牌。天眼查App显示…

vue3之实现响应式数据ref和reactive

用途 ref、reactive都是vue3提供实现响应式数据的方法 ref() 接受一个内部值&#xff0c;返回一个响应式的、可更改的ref对象&#xff0c;此对象只有一个指向其内部的属性.value ref可以说是简化版的reactive&#xff0c;与reactive的区别则是 ref是对某一个数据类型的单独…