React 核心源码解析
如果你问一个 React 面试题:”setState 之后发生了什么?”,标准答案应该是——React 创建 update 对象、调度更新、经过 reconciler 的 diff 过程、最后 commit 到 DOM。但如果追问:”reconciler 是怎么遍历 Fiber 树的?diff 算法的 O(n) 是怎么做到的?”,能回答清楚的人就少很多了。
这篇文章从 React 源码的调用链路出发,追溯从 ReactDOM.render 到真实 DOM 更新的完整过程。重点放在两个核心机制上:Fiber 架构为什么要替代 Stack Reconciler,以及 diff 算法做了哪些假设把复杂度从 O(n³) 降到 O(n)。
虚拟 DOM:不一定更快,但一定更可控
虚拟 DOM:不一定更快,但一定更可控
“虚拟 DOM 一定快吗?”——不一定。如果你直接 innerHTML 替换整个容器,虚拟 DOM 远比它快;但如果你精心手写了 appendChild 和 textContent 的最小更新,虚拟 DOM 的 diff 过程反而是额外开销。
虚拟 DOM 的价值不是”最快”,而是保证性能的下限——不管你写的更新逻辑有多粗糙,React 通过 diff 帮你找到最小变更集。同时它作为中间层,把开发者写的 JSX 翻译为平台无关的 VDOM 树,再由 Reconciler 转化为 Fiber 对象,最终由 Renderer 操作真实 DOM。
源码调用链路:从 render 到 commit
下面是 React 17 的调用链路(React 18 的 Concurrent Mode 增加了调度分支,但核心路径一致)。
render
1 | // react-dom/src/client/ReactDOMLegacy.js |
legacyRenderSubtreeIntoContainer
创建fiberroot的根节点
1 | // react-dom/src/client/ReactDOMLegacy.js |
updateContainer
1 | // react-reconciler/src/ReactFiberReconciler.old.js |
scheduleUpdateOnFiber
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
ensureRootIsScheduled
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
performSyncWorkOnRoot
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
renderRootSync
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
workLoopSync
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
performUnitOfWork
1 | // react-reconciler/src/ReactFiberWorkLoop.old.js |
beginWork
1 | // react-reconciler/src/ReactFiberBeginWork.old.js |
completeWork
1 | // react-reconciler/src/ReactFiberCompleteWork.old.js |
完整链路总结:
ReactDOM.render → legacyRenderSubtreeIntoContainer(创建 FiberRoot)→ updateContainer(创建 Update 并入队)→ scheduleUpdateOnFiber(调度)→ ensureRootIsScheduled(分同步/并发两条路)→ performSyncWorkOnRoot(同步)或 performConcurrentWorkOnRoot(并发)→ renderRootSync → workLoopSync → performUnitOfWork(循环处理每个 Fiber 节点)→ beginWork(向下调和)→ completeWork(向上归并,创建 DOM)→ commitRoot(提交到真实 DOM)
diff 算法:三个假设换来的 O(n)
React 如何把 diff 复杂度降下来?
Tree diff 的最优算法复杂度是 O(n³),对 1000 个节点做一次 diff 需要十亿次比较,不可用。React 做了三个启发式假设:
- 只对同级元素进行 diff:如果节点跨层级移动,直接删除重建,不做跨层级比较
- 类型不同的节点直接替换:
<div>变<span>,整棵子树删除重建 - 通过 key 标识同级子元素:同层级子元素通过 key 缓存实例,尽量复用而非重建
这三个假设把复杂度从 O(n³) 降到了 O(n)——好情况是 O(n),最差情况 O(mn)。
Fiber:为什么需要可中断的渲染
在 React 16 之前,协调器是 Stack Reconciler——递归遍历 Virtual DOM 树,整个过程是同步不可中断的。对于庞大的 DOM 树,一次 reconciliation 可能耗时上百毫秒,在这期间主线程被 JS 占用,任何交互、布局、渲染都会停止。
Fiber Reconciler 的核心改变:任务可中断、可恢复、有优先级。React 把 reconciliation 分解为一个个 Fiber 节点的工作单元,每次处理完一个单元后检查是否还有剩余时间,如果没有就交出主线程给浏览器处理用户交互。
实现上,React 使用 MessageChannel 模拟 requestIdleCallback 的行为(不用 requestIdleCallback 本身的原因:兼容性不够好,且 Chrome 的 50ms 调度间隔对 UI 渲染不够精细)。也没有用 Generator,因为 Generator 的 yield 会丢失调用栈。
调度机制
在 Concurrent Mode(React 18 默认开启)中,scheduler 包负责管理任务优先级。耗时任务被放入队列,以一定的节奏执行——高优先级的用户交互可以打断低优先级的渲染,渲染可以恢复。
使用:
1 | import { render } from "./render"; |
code:
1 |
|
1 | import { mount } from "./mount"; |
1 | import { patchProps } from "./patch"; |
1 | import { mount } from "./mount"; |
1 | import { mount } from './mount.js' |