react的diff源码

react 的 render 阶段,其中 begin 时会调用 reconcileChildren 函数, reconcileChildren 中做的事情就是 react 知名的 diff 过程

diff 算法介绍

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

diff 策略

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

  • 只对同级元素进行比较。如果出现跨层级的 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,看一下其源码:

 // 如果当前 fiber 节点为空,则直接将新的 ReactElement 内容生成新的 fiber,使mountChildFibers函数
  // 当前 fiber 节点不为空,则与新生成的 ReactElement 内容进行 diff 使用reconcileChildFibers函数

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

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

j

入口函数中,接收 returnFibercurrentFirstChildnewChildlanes 四个参数,其中,根据 newChid 的类型,我们主要关注几个比较常见的类型的 diff,单 React 元素的 diff、纯文本类型的 diff 和 数组类型的 diff。

 // 对新创建的 ReactElement 最外层是 fragment 类型单独处理,比较其 children
  // 对更新后的 React.Element 是单节点的处理
   // 更新后的 React.Element 是多节点的处理
  // 不符合以上情况都视为 empty,直接从父节点删除所有旧的子 Fiber

所以根据 ReactElement 类型走的不同流程如下: 

新内容为单元素

当新创建的节点 type 为 object 时,我们看一下其为 REACT_ELEMENT_TYPE 类型的 diff,即 placeSingleChild(reconcileSingleElement(...)) 函数。

先看一下 reconcileSingleElement 函数的源码:

echarts原理和源码解析 - 掘金 (juejin.cn)

根据源码得知,reconcileSingleElement 函数中,会遍历父 fiber 下所有的旧的子 fiber,寻找与新生成的 ReactElement 内容的 key 和 type 都相同的子 fiber。每次遍历对比的过程中:

  • 若当前旧的子 fiber 与新内容 key 或 type 不一致,对当前旧的子 fiber 添加 Deletion 副作用标记(用于 dom 更新时删除),继续对比下一个旧子 fiber
  • 若当前旧的子 fiber 与新内容 key 或 type 一致,则判断为可复用,通过 deleteRemainingChildren 对该子 fiber 后面所有的兄弟 fiber 添加 Deletion 副作用标记,然后通过 useFiber 基于该子 fiber 和新内容的 props 生成新的 fiber 进行复用,结束遍历。

若都遍历完没找到与新内容 key 或 type 子 fiber,此时父 fiber 下的所有旧的子 fiber 都已经添加了 Deletion 副作用标记,通过 createFiberFromElement 基于新内容创建新的 fiber 并将其 return指向父 fiber。

再来看 placeSingleChild 的源码:

placeSingleChild 中做的事情更为简单,就是将 reconcileSingleElement 中生成的新 fiber 打上 Placement 的标记,表示 dom 更新渲染时要进行插入。

所以对于 REACT_ELEMENT_TYPE 类型的 diff 总结如下: 

新内容为纯文本类型

当新创建节点的 typeof 为 string 或者 number 时,表示是纯文本节点,使用 placeSingleChild(reconcileSingleTextNode(...)) 函数进行 diff。

placeSingleChild 前面说过了,我们主要看 reconcileSingleTextNode 的源码:

新内容为纯文本时 diff 比较简单,只需要判断当前父 fiber 的第一个旧子 fiber 类型:

  • 当前 fiber 也为文本类型的节点时,deleteRemainingChildren 对第一个旧子 fiber 的所有兄弟 fiber 添加 Deletion 副作用标记,然后通过 useFiber 基于当前 fiber 和 textContent 创建新的 fiber 复用,将其 return 指向父 fiber
  • 否则通过 deleteRemainingChildren 对所有旧的子 fiber 添加 Deletion 副作用标记,然后 createFiberFromText 创建新的文本类型 fiber 节点,将其 return 指向父 fiber

所以对文本类型 diff 的流程如下: 

新内容为数组类型

上面所说的两种情况,都是一个或多个子 fiebr 变成单个 fiber。新内容为数组类型时,意味着要将一个或多个子 fiber 替换为多个 fiber,内容相对复杂,我们看一下 reconcileChildrenArray 的源码:

 

从上述代码我们可以得知,对于新增内容为数组时,react 会对旧 fiber 和 newChildren 进行遍历。

  1. 首先先对 newChildren 进行第一轮遍历,将当前的 oldFiber 与 当前 newIdx 下标的 newChild 通过 updateSlot 进行 diff,diff 的流程和上面单节点的 diff 类似,然后返回 diff 后的结果:
    • 如果 diff 后 oldFiber 和 newIdx 的 key 和 type 一致,说明可复用。根据 oldFiber 和 newChild 的 props 生成新的 fiber,通过 placeChild 给新生成的 fiber 打上 Placement 副作用标记,同时新 fiber 与之前遍历生成的新 fiber 构建链表树关系。然后继续执行遍历,对下一个 oldFiber 和下一个 newIdx 下标的 newFiber 继续 diff
    • 如果 diff 后 oldFiber 和 newIdx 的 key 或 type 不一致,那么说明不可复用,返回的结果为 null,第一轮遍历结束
  2. 第一轮遍历结束后,可能会执行以下几种情况:
    • 若 newChildren 遍历完了,那剩下的 oldFiber 都是待删除的,通过 deleteRemainingChildren 对剩下的 oldFiber 打上 Deletion 副作用标记
    • 若 oldFiber 遍历完了,那剩下的 newChildren 都是需要新增的,遍历剩下的 newChildren,通过 createChild 创建新的 fiber,placeChild 给新生成的 fiber 打上 Placement 副作用标记并添加到 fiber 链表树中。
    • 若 oldFiber 和 newChildren 都未遍历完,通过 mapRemainingChildren 创建一个以剩下的 oldFiber 的 key 为 key,oldFiber 为 value 的 map。然后对剩下的 newChildren 进行遍历,通过 updateFromMap 在 map 中寻找具有相同 key 创建新的fiber(若找到则基于 oldFiber 和 newChild 的 props创建,否则直接基于 newChild 创建),则从 map 中删除当前的 key,然后placeChild 给新生成的 fiber 打上 Placement 副作用标记并添加到 fiber 链表树中。遍历完之后则 existingChildren 还剩下 oldFiber 的话,则都是待删除的 fiber,deleteChild 对其打上 Deletion 副作用标记。

所以整体的流程如下: 

diff 后的渲染

diff 流程结束后,会形成新的 fiber 链表树,链表树上的 fiber 通过 flags 字段做了副作用标记,主要有以下几种:

  1. Deletion:会在渲染阶段对对应的 dom 做删除操作
  2. Update:在 fiber.updateQueue 上保存了要更新的属性,在渲染阶段会对 dom 做更新操作
  3. Placement:Placement 可能是插入也可能是移动,实际上两种都是插入动作。react 在更新时会优先去寻找要插入的 fiber 的 sibling,如果找到了执行 dom 的 insertBefore 方法,如果没有找到就执行 dom 的 appendChild 方法,从而实现了新节点插入位置的准确性

在 completeUnitWork 阶段结束后,react 会根据 fiber 链表树的 flags,构建一个 effectList 链表,里面记录了哪些 fiber 需要进行插入、删除、更新操作,在后面的 commit 阶段进行真实 dom 节点的更新,下一章将详细讲述 commit 阶段。

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

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

相关文章

md5绕过

文章目录 \\和\\\md5数组绕过科学计数法绕过双md加密md5碰撞Hash长度攻击 下面会以同一道题给大家演示: (题目来源与nssctf) 和 在php代码中我们会看到和,虽然两个都是表示相等,但是在细节上会有所部区别 :是弱比较,只…

0201安装报错-hbase-大数据学习

1 基础环境简介 linux系统:centos,前置安装:jdk、hadoop、zookeeper,版本如下 软件版本描述centos7linux系统发行版jdk1.8java开发工具集hadoop2.10.0大数据生态基础组件zookeeper3.5.7分布式应用程序协调服务hbase2.4.11分布式…

Sora 作者被曝读博期间仅发表两篇论文,我们是否需要重塑科研价值观?

众所周知,在当今学术界,论文数量和产出速度常常被视为研究者生产力和学术成就的重要标尺。笔者也面试过很多博士生候选人,大家普遍会以自己读博期间发表过10几篇甚至几十篇论文而骄傲,很少有候选人会强调自己读博期间虽然发表论文…

Matlab|基于目标级联法的微网群多主体分布式优化调度

目录 主要内容 1.1 上层微网群模型 1.2 下层微网模型 部分程序 实现效果 下载链接 主要内容 本文复现《基于目标级联法的微网群多主体分布式优化调度》文献的目标级联部分, 建立微网群系统的两级递阶优化调度模型: 上层是微网群能量调度中心优化调度…

java中的字符串比较(题目作示例)

错误的代码 import java.util.Scanner; public class one {public static void main(String[] args) {Scanner scnew Scanner(System.in);String b"47568";int i0;for ( i 0; i <3; i){String asc.next();if(ab){System.out.println("密码正确&#xff0c;登…

在外包公司干了4年,技术退步2年...

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近6年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&#xf…

白话模电:1.绪论与半导体(考研面试常问问题)

一、什么是信号&#xff1f;什么是电信号&#xff1f; 信号反映消息的物理量&#xff0c;电信号是反应电压或电流变化的物理量。 二、什么是模拟信号&#xff1f;什么是数字信号&#xff1f; 模拟信号是时间和数值上均连续的信号&#xff0c;数字信号是时间和数值上均离散的信号…

[HackMyVM]靶场 Zeug

kali:192.168.56.104 主机发现 arp-scan -l # arp-scan -l Interface: eth0, type: EN10MB, MAC: 00:0c:29:d2:e0:49, IPv4: 192.168.56.104 Starting arp-scan 1.10.0 with 256 hosts (https://github.com/royhills/arp-scan) 192.168.56.1 0a:00:27:00:00:05 (Un…

Seata 2.x 系列【5】直接部署

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 本系列Seata 版本 2.0.0 本系列Spring Boot 版本 3.2.0 本系列Spring Cloud 版本 2023.0.0 源码地址&#xff1a;https://gitee.com/pearl-organization/study-seata-demo 文章目录 1. 概述2. 环境要…

学习嵌入式C语言要掌握到什么程度?

学习嵌入式C语言要掌握到什么程度&#xff1f; 在开始前我分享下我的经历&#xff0c;我刚入行时遇到一个好公司和师父&#xff0c;给了我机会&#xff0c;一年时间从3k薪资涨到18k的&#xff0c; 我师父给了一些 电气工程师学习方法和资料&#xff0c;让我不断提升自己&#…

Java注解介绍

Java注解 注解介绍元注解RetentionTargetDocumentedInherited接口类测试结果 注解介绍 Java注解&#xff08;Annotation&#xff09;是一种元数据&#xff08;Metadata&#xff09;的形式&#xff0c;它可以被添加到Java代码中的类、方法、变量、参数等元素上&#xff0c;以提…

一分钟带你入门Selenium入门!【建议收藏】

Selenium入门 欢迎阅读Selenium入门讲义&#xff0c;本讲义将会重点介绍Selenium的入门知识以及Selenium的前置知识。 自动化测试的基础 在Selenium的课程以前&#xff0c;我们先回顾一下软件测试的基本原理&#xff0c;为我们进一步完成Selenium自动化测试做好准备。 软件…

考研数学|张宇30讲,搭配什么基础题?

如果基础跟的是张宇&#xff0c;那么基础做的题目要根据自己的题目来决定 题集的选择最好不要太难&#xff0c;而且基础也不用做太多题目&#xff0c;以数学知识点的运用&#xff0c;培养做题感觉为主。 张宇老师的课程在基础阶段也有配套的课程&#xff0c;就是《张宇基础30…

关于进程和线程

目录 前言: 1进程: 1.1定义&#xff1a; 1.1.1进程是操作系统分配资源的基本单元&#xff0c;拥有自己的独立空间和资源。 1.1.2每个进程都有一个唯一的PID&#xff08;进程标识符&#xff09;来标识。 1.2进程间通信&#xff1a; 1.2.1进程不是孤立的&#xff0c;它们之…

搜索引擎都没流量啦,官网建设还有啥意义?

百度等搜索引擎都没啥流量了&#xff0c;再建设官网还有啥用&#xff1f;如果你把官网定位于获客&#xff0c;那真的没啥太大用处&#xff0c;但是官网不仅仅是用来获客的。 一、搜索引擎的流量被稀释了 搜索引擎流量减少的原因有多个&#xff0c; 1. 社交媒体的崛起&#xf…

电机控制系列模块解析(第八篇)—— 高频注入法

一、基本原理 在电机控制领域&#xff0c;高频方波注入法常用于估计转子的位置和转速。其工作原理是每隔一定的时间间隔在电机输出电压上叠加一个电压脉冲。然后&#xff0c;在两相静止坐标系下或者同步旋转坐标系下对采样时刻的电压和电流进行相应的计算&#xff0c;以实现对…

JS-04-javaScript数据类型和变量

一、数据类型 计算机能处理的远不止数值&#xff0c;还可以处理文本、图形、音频、视频、网页等各种各样的数据&#xff0c;不同的数据&#xff0c;需要定义不同的数据类型。在JavaScript中定义了以下几种数据类型&#xff1a; 1-1、Number JavaScript不区分整数和浮点数&…

关于Vivado的实施过程、SDC和XDC约束支持、Vivado实施子流程、Tcl API支持脚本

关于Vivado的实施过程 AMD Vivado™设计套件可实现以下AMD设备体系结构&#xff1a;AMD Versal™自适应计算加速平台&#xff08;自适应SoC&#xff09;&#xff0c;AMDUltraScale™、AMD UltraScale™和AMD 7系列FPGA。各种设计来源如下支持&#xff0c;包括&#xff1a; •…

【视频图像取证篇】模糊图片复原车牌号技术原理和实战应用小结

【视频图像取证篇】模糊图片复原车牌号技术原理和实战应用小结 模糊图片复原车牌号常用的技术原理和实战应用—【蘇小沐】 &#xff08;一&#xff09;运动模糊视频图像 由于各种各样的原因&#xff0c;主体或者拍摄设备运动共同造成的视频图像模糊等。 1、快门速度 快门速…

Vue事件处理:.passive修饰符与应用场景

.passive修饰符 passive这个修饰符会执行默认方法。你们可能会问&#xff0c;明明默认执行为什么会设置这样一个修饰符。这就要说一下这个修饰符的本意了。 浏览器只有等内核线程执行到事件监听器对应的JavaScript代码时&#xff0c;才能知道内部是否会调用preventDefa…