【React源码 - Diff算法】

介绍

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

在reconcileChildFibers中,主要是通过newChild的类型以及type来判断执行那个函数来更新fiber,其中主要类型分为非空对象、字符串/数字、数组、可迭代类型。
在这里插入图片描述
其中我们主要对非空对象的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) {// ...省略casedefault: {if (child.elementType === element.type) {// 将该fiber及其兄弟fiber标记为删除deleteRemainingChildren(returnFiber, child.sibling);// type相同则表示可以复用const existing = useFiber(child, element.props);// 更新复用节点的refexisting.ref = coerceRef(returnFiber, child, element);existing.return = returnFiber;// 返回复用的fiberreturn existing;}// type不同则跳出switchbreak;}}// 代码执行到这里代表: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删除。

代码流程可以简述为:
在这里插入图片描述

多节点对比

这里我们通过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 个初始化序列:

在这里插入图片描述
接下来第一次循环, 会遍历公共序列A,B, 生成的 fiber 节点fiber(A), fiber(B)可以复用.
在这里插入图片描述
最后第二次循环, 会遍历剩余序列E,C,X,Y:

  • 生成的 fiber 节点fiber(E), fiber©可以复用. 其中fiber©节点发生了位移(打上Placement标记).
  • fiber(X), fiber(Y)是新增(打上Placement标记).
  • 同时oldFiber序列中的fiber(D)节点确定被删除(打上Deletion标记).
    在这里插入图片描述
    整个主干逻辑就介绍完了, 接下来贴上完整源码

第一次循环:

// 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 树的比较。
在这里插入图片描述
如果DOM节点前后更新跨越了层级,React则不会复用,直接新建节点并删除原来节点:

在这里插入图片描述
2、类型比较
当比较两个节点时,首先检查它们的类型。如果类型不同,React 会销毁旧节点,并建立新节点。这种类型检查适用于不同的HTML标签或不同的组件类型。

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

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

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

优化策略

基于React对于传递Diff的优化总结,我们可以通过以下方式来进行优化,简化对比:

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

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

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

相关文章

python中Pytest常用的插件

前言 除了框架本身提供的功能外&#xff0c;Pytest还支持上百种第三方插件&#xff0c;良好的扩展性可以更好的满足大家在用例设计时的不同需求。本文将为大家详细介绍下面5项常用的插件。 1. 用例依赖 编写用例的时候&#xff0c;我们会注意用例之间的独立性&#xff0c;但部分…

嵌入式学习-网络编程-Day5

思维导图 select完成tcp并发服务器模型&#xff1a; 服务器端 #include <myhead.h> #define SER_PORT 8888 #define SER_IP "192.168.122.153"int main(int argc, const char *argv[]) {//1.创建套接字int sfd socket(AF_INET,SOCK_STREAM,0/*IPPROTO_TCP*…

Linux系统三剑客之grep和正则表达式的介绍(一)

1.正则表达式 目录 1.正则表达式 1.什么是正则表达式 &#xff1f; 2.正则表达式的使用场景 3.正则表达式字符表示 4.它们之间的区别 2.grep命令 作用&#xff1a; 语法&#xff1a; 说明&#xff1a; 选项&#xff1a;options 重点 实例 3.后面的下次再更新。 …

MongoDB Compass当前版本及历史版本下载安装

mongoDB compass 当前版本下载 官网 https://www.mongodb.com/try/download/compass 官网下载一般只能下载最新版本。 github https://github.com/mongodb-js/compass MongoDB Compass与MongoDB的版本对应关系 MongoDB CompassMongoDB1.9.12MongoDB 2.6.11 Community

QT通过QPdfWriter类实现pdf文件生成与输出

一.QPdfWriter类介绍 本文代码工程下载地址&#xff1a; https://download.csdn.net/download/xieliru/88736664?spm1001.2014.3001.5503 QPdfWrite是一个用于创建PDF文件的类&#xff0c;它是Qt库的一部分。它提供了一些方法和功能&#xff0c;使您能够创建和写入PDF文件。…

PHP反序列化总结4--原生类总结

原生类的简要介绍以及原生类和反序列化的关系 PHP 原生类指的是 PHP 内置的类&#xff0c;它们可以直接在 PHP 代码中使用且无需安装或导入任何库&#xff0c;相当于代码中的内置方法例如echo &#xff0c;print等等可以直接调用&#xff0c;但是原生类就是可以就直接php中直接…

翻译: Streamlit从入门到精通 显示图表Graphs 地图Map 主题Themes 二

Streamlit从入门到精通 系列&#xff1a; 翻译: Streamlit从入门到精通 基础控件 一 1. 使用Streamlit显示图表Graphs 1.1 为什么我们需要可视化&#xff1f; 数据可视化通过将数据整理成更容易理解的格式来讲述故事&#xff0c;凸显趋势和异常点。好的可视化能够讲述一个故…

如何定位和优化程序CPU、内存等性能之巅

目录 摘要 引言 关注 指标 正文 定位CPU瓶颈 定位内存瓶颈 定位IO瓶颈 总结 摘要 性能优化指在不影响系统运行正确性的前提下&#xff0c;使之运行得更快&#xff0c;完成特定功能所需的时间更短&#xff0c;或拥有更强大的服务能力。本文将介绍性能优化的基本概念以…

Logback的配置文件,你看懂了吗

参考文章一 参考文章二 configuration是XML文件根元素。root和logger可视为同一类&#xff0c;都是日志设置&#xff1b;root是日志的全局设置&#xff0c;而logger可以单独设置某一些包和类的日志输出。appender配置日志格式、如何过滤、文件处理等。property和contextName元…

(N-140)基于springboot,vue协同过滤推荐算法个性化购物商城

开发工具&#xff1a;IDEA 服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8 项目构建&#xff1a;maven 数据库&#xff1a;mysql5.7 系统分前后台&#xff0c;项目采用前后端分离 前端技术&#xff1a;vueelementUI 服务端技术&#xff1a;springbootmybatisredis 本…

yarn包管理器在添加、更新、删除模块时,在项目中是如何体现的

技术很久不用&#xff0c;就变得生疏起来。对npm深受其害&#xff0c;决定对yarn再整理一遍。 yarn包管理器 介绍安装yarn帮助信息最常用命令 介绍 yarn官网&#xff1a;https://yarn.bootcss.com&#xff0c;学任何技术的最新知识&#xff0c;都可以通过其对应的网站了解。无…

下一代 Vue3 Devtools 正式开源

什么是 Vue DevTools Vue DevTools 是一个旨在增强 Vue 开发人员体验的工具,它提供了一些功能来帮助开发者更好地了解 Vue 应用程序。 Vue DevTools:Unleash Vue Developer Experience. Enhance your Vue development journey with an amazing experience! 典型的功能特征包…