news 2026/10/26 10:11:45

React 源码揭秘:单节点 Diff 与 `reconcileSingleElement` 的复用判定逻辑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
React 源码揭秘:单节点 Diff 与 `reconcileSingleElement` 的复用判定逻辑
  • 文档
  • 前端

【免费下载链接】just-react

「React技术揭秘」 一本自顶向下的React源码分析书

项目地址:https://gitcode.com/gh_mirrors/ju/just-react
点击查看免费下载

本篇技术指南聚焦「React 技术揭秘」(just-react)仓库中 docs/diff/one.md 讲解的核心主题——单一节点(单元素)的 Diff 算法,即reconcileSingleElement如何判断新旧JSX对应的DOM 节点是否可以复用。文中将结合本仓库 docs/diff/prepare.md、docs/process/beginWork.md 等章节,从调用入口、判定流程、删除语义到练习题,完整梳理单节点 Diff 的源码实现与设计动机。读完本文,你将能够徒手写出reconcileSingleElement的核心判定逻辑,并准确解释key与type在节点复用中的各自作用。

从reconcileChildFibers到reconcileSingleElement

在 docs/diff/prepare.md 中我们了解到,Diff 算法是Reconciler的核心:对于update的组件,beginWork会将其与该组件上次更新时对应的Fiber 节点(即current fiber)进行比较,比较的结果生成新的Fiber 节点(即workInProgress fiber)。

Diff 的入口函数是reconcileChildFibers,它会根据newChild(即本次更新的JSX 对象)的类型分发到不同的处理函数:

function reconcileChildFibers( returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, ): Fiber | null { const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { // object 类型,可能是 REACT_ELEMENT_TYPE 或 REACT_PORTAL_TYPE switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: // 调用 reconcileSingleElement 处理 // ...其他 case } } if (typeof newChild === 'string' || typeof newChild === 'number') { // 调用 reconcileSingleTextNode 处理 // ...省略 } if (isArray(newChild)) { // 调用 reconcileChildrenArray 处理(多节点 Diff,见 docs/diff/multi.md) // ...省略 } // 以上都没有命中,删除节点 return deleteRemainingChildren(returnFiber, currentFirstChild); }

按照同级节点的数量,Diff 被划分为两类:

  1. 单节点 Diff:newChild类型为object、number或string,代表同级只有一个节点,进入reconcileSingleElement/reconcileSingleTextNode;
  2. 多节点 Diff:newChild类型为Array,同级有多个节点,进入reconcileChildrenArray(详见 docs/diff/multi.md)。

本文讨论的正是第一种情况:以类型object(REACT_ELEMENT_TYPE)为例,它最终会进入reconcileSingleElement。

reconcileSingleElement做了什么

reconcileSingleElement是单节点 Diff 的核心函数,它接收三个参数:

  • returnFiber:当前正在执行beginWork的父级Fiber 节点;
  • currentFirstChild:上一次更新中第一个子Fiber 节点(单链表头);
  • element:本次更新的ReactElement(即JSX 对象)。

其完整执行流程可以概括为两步:

  1. 遍历上一次更新的兄弟 fiber 链,寻找本次element能否复用的DOM 节点;
  2. 根据能否复用决定返回:能复用则返回复用的fiber,不能复用则删除旧节点并新建Fiber(源码中创建新Fiber的部分省略)。
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) { // ...省略 case default: { if (child.elementType === element.type) { // type 相同则表示可以复用 // 返回复用的 fiber return existing; } // type 不同则跳出 switch break; } } // 代码执行到这里代表:key 相同但是 type 不同 // 将该 fiber 及其兄弟 fiber 标记为删除 deleteRemainingChildren(returnFiber, child); break; } else { // key 不同,将该 fiber 标记为删除 deleteChild(returnFiber, child); } child = child.sibling; } // 创建新 Fiber,并返回 ...省略 }

可以看到,这个函数做的事情就是拿着本次更新的element,遍历上一次更新的fiber单链表,判断能否复用,并在决定不复用时清理旧节点。

React 预设的三个限制(复用判定的前提)

在 docs/diff/prepare.md 中强调过:React 为了把 Diff 的算法复杂度从 O(n³) 降下来,预设了三条限制:

  1. 只对同级元素进行 Diff——节点跨越层级就不会尝试复用;
  2. 两个不同类型的元素会产生出不同的树——元素由div变为p,React 会销毁div及其子孙节点并新建p的树;
  3. 开发者可以通过key prop暗示哪些子元素在不同的渲染下能保持稳定。

单节点 Diff 的代码正是这三条限制的直接体现:

从代码可以看出,React 通过先判断key是否相同,如果key相同则判断type是否相同,只有key与type都相同时,一个DOM 节点才能复用。

删除语义的两个关键细节

代码中有两处删除调用,它们的语义差异值得细读:

  • 当child !== null且key 相同且type 不同时,执行deleteRemainingChildren(returnFiber, child),将child及其兄弟 fiber 全部标记删除;
  • 当child !== null且key 不同时,仅执行deleteChild(returnFiber, child),将child这一个 fiber 标记删除,然后child = child.sibling继续遍历。

为什么会有这种差别?考虑如下例子:

// 当前页面显示的 ul > li * 3 // 这次需要更新的 ul > p

本次更新只有一个p,属于单一节点的 Diff。在reconcileSingleElement中会遍历之前的 3 个fiber(对应 DOM 为 3 个li),寻找本次的p是否可以复用其中某个fiber的DOM。

  • 当key相同且type不同时:代表已经找到了本次p对应的上次的fiber,但p与li的type不同,不能复用。既然唯一可能复用的候选已经排除了,剩下的fiber就都没有机会了,所以需要把child及其所有兄弟都标记删除(deleteRemainingChildren)。
  • 当key不同时:只代表当前遍历到的这个fiber不能被p复用,后面还有兄弟fiber尚未遍历到,所以只标记当前这一个fiber删除(deleteChild),并继续向后遍历。

一句话总结:type不同意味着"没希望了,全删";key不同只是"这一个不行,再看看下一个"。

从beginWork看 Diff 的触发时机

单节点 Diff 并非凭空发生,它由 render 阶段"递"过程中的beginWork驱动。在 docs/process/beginWork.md 中可以看到,对于常见的FunctionComponent/ClassComponent/HostComponent,beginWork最终会进入reconcileChildren:

export function reconcileChildren( current: Fiber | null, workInProgress: Fiber, nextChildren: any, renderLanes: Lanes ) { if (current === null) { // 对于 mount 的组件:创建新的子 Fiber 节点 workInProgress.child = mountChildFibers( workInProgress, null, nextChildren, renderLanes, ); } else { // 对于 update 的组件:执行 Diff,将比较的结果生成新 Fiber 节点 workInProgress.child = reconcileChildFibers( workInProgress, current.child, nextChildren, renderLanes, ); } }

也就是说,Diff 只发生在update(current !== null)场景;mount时走的是mountChildFibers,直接创建全新的子Fiber 节点。reconcileChildFibers与mountChildFibers逻辑基本一致,唯一区别是前者会为生成的Fiber 节点带上effectTag,而后者不会。

结合 docs/process/doubleBuffer.md 的双缓存机制可以更完整地理解:current Fiber 树对应屏幕上已显示的内容,workInProgress Fiber 树在内存中构建,构建完成后通过切换current指针完成DOM更新。reconcileSingleElement返回的"复用的 fiber"正是workInProgress Fiber 树中的节点,它通过alternate属性与current fiber相连。

实战:四道练习题巩固复用判定

请判断如下JSX 对象对应的DOM 元素是否可以复用:

// 习题1 更新前 <div>ka song</div> // 更新后 <p>ka song</p> // 习题2 更新前 <div key="xxx">ka song</div> // 更新后 <div key="ooo">ka song</div> // 习题3 更新前 <div key="xxx">ka song</div> // 更新后 <p key="ooo">ka song</p> // 习题4 更新前 <div key="xxx">ka song</div> // 更新后 <div key="xxx">xiao bei</div>

公布答案:

  • 习题1:不能复用。未设置key prop时默认key = null,所以更新前后key相同(都为null),但更新前type为div、更新后为p,type改变则不能复用。
  • 习题2:不能复用。更新前后key改变("xxx"→"ooo"),不需要再判断type,直接判定不能复用。
  • 习题3:不能复用。更新前后key改变("xxx"→"ooo"),不需要再判断type,直接判定不能复用。注意本题type也变了,但判定在key这一层就已经结束。
  • 习题4:可以复用。更新前后key与type都未改变,判定为可复用;但children由ka song变为xiao bei,复用的DOM 节点的子元素仍然需要更新。

总结:单节点 Diff 的判定口诀

单节点 Diff(reconcileSingleElement)的核心逻辑可以浓缩为一句话:

先比key,再比type;key不同逐个删,key同type不同全删重建,key、type都相同才复用。

后续你可以继续阅读 docs/diff/multi.md 学习同级多个节点(Array类型)的 Diff:那里会引入两轮遍历,并用lastPlacedIndex与existingChildrenMap 来判断节点的移动,是 Diff 算法中最精髓的部分。

  • 文档
  • 前端

【免费下载链接】just-react

「React技术揭秘」 一本自顶向下的React源码分析书

项目地址:https://gitcode.com/gh_mirrors/ju/just-react
点击查看免费下载

相关推荐

上一篇:WaveTools鸣潮工具箱完全指南:三步实现120FPS高帧率与画质优化
下一篇:Hotkey Detective:Windows系统热键占用检测的完整解决方案

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 5:17:52

【OPD】On-Policy Distillation Survery

note OPD 的监督更多来自 teacher 分布或 teacher feedback&#xff1b;RL 更多来自 reward/verifierOPD 解决长轨迹误差累积&#xff08;exposure bias&#xff09;&#xff0c;让学生模型自己 rollout&#xff0c;再由教师/奖励模型/验证器对学生真实轨迹提供监督信号&#…

作者头像 李华
网站建设 2026/10/16 0:03:10

基于agency-agents的多智能体协作架构与工程实践

看到“agency-agents”这个词&#xff0c;我第一反应不是某个具体开源仓库&#xff0c;而是一种正在被反复验证的多智能体协作模式&#xff1a;把几个擅长不同事情的 AI Agent&#xff0c;像一家小型公司那样组织起来&#xff0c;让它们各自认领角色、互相传递中间结果、最后拼…

作者头像 李华