当前位置: 首页 > news >正文

深入 React19 Diff 算法

一、为什么 React 需要 Diff 算法

早期前端如果直接操作 DOM:

div.innerHTML = newHTML

问题:

  1. DOM 操作极其昂贵
  2. 无法知道哪些节点真的变化
  3. 频繁重绘

如果使用​传统树 diff 算法​:

复杂度:O(n^3)

浏览器根本无法接受。

因此 React 提出 对 diff 算法做了各种优化,最终复杂度 O(n)。

React 的核心约束:

约束 1:不同类型节点一定不同

<div />
<span />

直接删除重建。

约束 2:同层节点对比

React 不会跨层比较

只比较:

oldChildren
newChildren

约束 3:key 用来稳定节点

{list.map(item => (<Item key={item.id}/>
))}

key 让 React 知道:

这个节点是不是同一个

二、架构层面看 diff

React19 的更新流程:

setState↓
scheduleUpdateOnFiber↓
render阶段↓
beginWork↓
reconcileChildren  ← diff发生在这里↓
completeWork↓
commit阶段↓
DOM mutation

源码位置:react-reconciler/src/ReactChildFiber.js

  • 核心函数:reconcileChildren、reconcileChildFibers
  • 创建 Fiber:createFiberFromElement
  • 复用 Fiber:useFiber
  • 处理数组:reconcileChildrenArray

React Diff 的核心逻辑:

oldFiberTree(current fiber node)↓
newReactElementTree(jsx)↓
生成 newFiberTree(wip fiber node)↓
打 flags(等到 commit 再处理)

Fiber 结构(简化):

type Fiber = {tag: WorkTagkey: null | stringtype: anystateNode: anyreturn: Fiberchild: Fibersibling: FiberpendingPropsmemoizedPropsalternate: Fiberflags
}

1 alternate

current fiber 和 workInProgress fiber 形成 双缓存树

2 flags

记录需要执行的操作:Placement | Update | Deletion

commit 阶段使用。

三、最简单的 Diff:单节点

如果 DOM 更新后,还是一个节点的话,那么就采用单节点 diff。

function reconcileSingleElement(returnFiber,currentFirstChild,element
)

逻辑:

Step1:寻找 key 相同节点 child.key === element.key(如果都没有设置 key,那么都为 null,也属于相同)

Step2:type 是否相同 child.type === element.type

Step3:复用 Fiber useFiber(oldFiber)(如果内部文本不同,直接将内部文本节点更新即可)

否则:删除旧节点,创建新节点

四、数组 Diff

如果 DOM 更新后,为多个节点的话,就采用多节点 diff(数组 diff) 。

真正复杂的是:reconcileChildrenArray。React 团队认为,对节点更新操作的情况往往要多于对节点“新增、删除、移动”的操作。因此,源码逻辑分为 两轮遍历。

第一轮:从左到右对比

React 会先 顺序对比,目的就是希望尽可能的复用单节点。

  • 如果新旧子节点的 key 和 type 都相同,直接复用
  • 如果新旧子节点的 key 相同,但是 type 不相同,这时会根据 ReactElement 来生成一个全新的 Fiber,旧的 Fiber 被放入到 deletions 数组里面,之后统一删除。但是此时遍历并不会终止
  • 如果新旧子节点的 key 和 type 都不相同,结束遍历

旧:

A B C D

新:

A B E

流程(同时对比 key 和 type):

A = A ✔
B = B ✔
C ≠ E ✘

停止。

这一步叫:快速路径(Fast Path)

源码:

while (oldFiber && newIdx < newChildren.length)

复杂度:O(n)。

第二轮:构建 key map,遍历新 children

如果第一轮遍历被提前终止了,那么意味着有新的 React 元素或者旧的 FiberNode 没有遍历完,此时就会采用第二轮遍历。

情况一:有旧节点剩余,放入 deletions 数组中之后删掉。

情况二:有新节点出现,创建新的 fiber。

情况三:

  • 新旧子节点都有剩余:会将旧节点剩余的 FiberNode 节点放入一个 map 里面,遍历剩余的新节点,然后从 map 中去寻找能够复用的 FiberNode 节点,如果能够找到就复用。(移动的情况)
  • 如果不能找到就新增。然后如果剩余新节点都遍历完了,map 结构中还有剩余的 Fiber 节点,就将这些 Fiber 节点添加到 deletions 数组里面,之后统一做删除操作

React19 这里没有使用像 Vue3 那样的双端 diff 算法,具体原因 React 直接写在了源码内:

由于双端 diff 需要向前查找节点,但每个 FiberNode 节点上都没有反向指针,即前一个 FiberNode 通过 sibling 属性指向后一个 FiberNode,只能从前往后遍历,而不能反过来,因此该算法无法通过双端搜索来进行优化。

React 想看下现在用这种方式能走多远,如果这种方式不理想,以后再考虑实现双端 diff。React 认为对于列表反转和需要进行双端搜索的场景是少见的,所以在这一版的实现中,先不做额外的优化。

五、diff 和调度的关系

React Diff 不是一次完成。

因为 React 有:

Concurrent Rendering

Fiber 可以:

中断
恢复
优先级调度

例如:

render 10000 节点

React 可以:

render 200
yield
render 200

Diff 过程变成:

可中断计算

六、完整 Diff 流程图

beginWork││▼
reconcileChildren││▼
reconcileChildFibers││├── 单节点 diff│├── 文本节点 diff│└── 数组 diff││├── 第一轮:顺序比较│├── 第二轮:构建 map│└── 第三轮:查找复用 / 新建││▼打 flags│▼completeWork│▼commit
http://www.jsqmd.com/news/440877/

相关文章:

  • GitLab CE 16.x在CentOS7上的性能优化指南:让你的服务器跑得更快
  • MathType6.0与Word2016的无缝整合:从宏录制到自定义功能区
  • Gunicorn 部署 Flask 应用避坑指南:从零到生产环境的完整流程
  • 七级笔记(文章都是引用的,主播不是原作者)
  • StructBERT模型VMware虚拟机沙箱环境部署指南
  • 倾斜模型+BIM场景搭建避坑指南:LSV模型缩放/定位的7个隐藏技巧
  • NETSOL 32Mb串行外设接口SPI MRAM
  • 从零理解Golang channel:图解有缓存/无缓存的底层差异与应用选择
  • 快速搞定PyTorch 2.9:预装CUDA镜像,实测下载速度与稳定性
  • AM5728实战:从零构建ARM Ubuntu 20.04定制化根文件系统
  • Redisson看门狗机制深度解析:如何用10秒心跳避免分布式锁死锁
  • 【RISC-V 2026 C驱动开发权威指南】:全球首发首份ISO/IEC JTC 1联合草案解读与迁移路线图
  • google play必须具备举报用户功能
  • 深入解析FFmpeg中MOV封装的内部机制与实现
  • 超越SPSS:用R语言做ROC曲线分析的进阶技巧(附完整代码)
  • PySide vs PyQt:从许可证到性能的全面对比指南
  • PyTorch预训练权重加载与冻结实战指南:从基础到进阶
  • 腾讯混元1.8B量化版体验:HY-1.8B-2Bit-GGUF镜像快速入门与创作实测
  • Python实战:用ARIMA预测电力负荷(附完整代码与数据集)
  • 储能系统HIL测试实战:Speedgoat实时仿真机配置与避坑指南
  • HY-MT1.5-1.8B翻译模型实战测评:从短句到长文档的翻译效果
  • 禅道企业微信消息推送优化:实现自定义内容与精准@成员提醒
  • FreeRTOS任务调度与SPI主从通信:S32K144在IAR中的完整实现流程
  • DolphinScheduler租户配置避坑指南:从‘tenant not exists‘看用户权限体系设计
  • 深入解析Xilinx Artix-7 xc7a35t FPGA架构与实战应用(三)
  • Java项目中策略模式的使用方法:从零入门到实战落地(小白友好版)
  • 告别996:GitHub Copilot将我的开发效率提升300%的实战记录
  • FPGA篇---DSP Slice:FPGA 的“算术加速引擎”
  • FPGA篇---Global Clock Network (全局时钟网络):FPGA 的“中枢神经系统”
  • 语音交互(Voice-Based Communication)