🧑💻 写在开头
之前写了几篇关于react的原理文章,但是后面比较忙就没有后续了,最近在准备面试,做编辑器的时候用到了自定义渲染器,在以前的时候我看过react原理的很多文章,一直想总结成自己的文章方便复习,现在又重新捡起来,继续往下写
点赞 + 收藏 === 学会 🤣🤣🤣
在使用 React 的时候,我们经常会写这样的代码:
ini
setCount(c => c + 1);
setCount(c => c + 1);
最终结果 count
只更新一次,但值却正确地加了 2
。这背后涉及到 React 的 Update 对象 、UpdateQueue 队列 、调度机制 以及 批量更新。
本文就带你从源码角度完整梳理一遍 React 更新的完整流程,并解释为什么 React17 里 setTimeout/Promise
不是批量的,而 React18 却可以自动批量。
🥑 你能学到什么?
读完这篇文章,你将收获:
- Update 对象的结构和作用
- UpdateQueue 如何统一管理更新
- React 更新的四个阶段:触发 → 调度 → render → commit
- 批量更新的本质
- 为什么 React17 里异步更新不是批量的,而 React18 变了
isBatchingUpdates
这个标志位到底在什么时候是true
✍️系列文章react实现原理系列
- 【react18原理探究实践】使用babel手搓探索下jsx的原理
- 【react18原理探究实践】上手调试源码探究jsx原理
- 【react18原理探究实践】图解react几个核心包之间的关联
- 【react18原理探究实践】react启动流程,其实就是创建三大全局对象
- 【react18原理探究实践】JS中的位运算&react中的lane模型
- 【react18原理探究实践】更新调度:如何统一更新
一、Update 对象
每次调用 setState
或 dispatch
,React 内部都会生成一个 Update 对象:
js
export type Update<State> = {
eventTime: number, // 事件触发时间
lane: Lane, // 优先级
tag: 0 | 1 | 2 | 3, // 更新类型(更新/替换/强制/捕获)
payload: any, // 载荷,可能是值,也可能是函数
callback: (() => void) | null, // 更新完成后的回调
next: Update<State> | null, // 指向下一个 update,形成链表
};
关键点:
Update
不是直接存新值,而是存一条"指令" → 最终 state 如何计算。setState(1)
→ payload 是值setState(prev => prev + 1)
→ payload 是函数forceUpdate()
→ payload 为空
👉 这样 React 就能先把更新收集起来,最后统一计算。
二、UpdateQueue 更新队列
每个 Fiber 节点都有一个 updateQueue,用链表保存挂在该组件上的所有更新。
- 多次
setState
→ 会依次挂多个 Update。 - 在 render 阶段统一处理这些更新,得到新的 state。
这就解释了为什么 React 能把多个 setState
合并成一次渲染。
三、React 更新完整流程
整个更新链路可简化为 四个阶段:
-
触发更新
- 调用
setState
,生成 update,挂到 fiber 的 updateQueue - 根据 lane(优先级)标记更新紧急程度
- 调用
-
调度(Scheduler)
- 根据优先级决定何时渲染
- 高优先级立即执行,低优先级可能延迟
-
render 阶段
- 从根 fiber 构建新的 fiber 树(workInProgress)
- 执行
processUpdateQueue
→ 统一应用更新队列,计算新的 state
-
commit 阶段
- before mutation →
getSnapshotBeforeUpdate
- mutation → 应用 DOM 变更
- layout → 执行
useLayoutEffect
/componentDidMount
- before mutation →
最终才更新 UI。
四、批量更新(Batched Updates)
例子:
ini
setCount(c => c + 1);
setCount(c => c + 1);
流程:
-
两次 setState → 生成两个 update,挂到同一个 queue。
-
render 阶段依次应用:
- baseState = 0
- update1 → 1
- update2 → 2
-
commit 阶段一次性更新 UI
👉 结果:渲染一次,state = 2。
五、React17:为什么 setTimeout/Promise
不是批量?
React17 的批量更新依赖于 合成事件系统:
- React 分发合成事件时,用
batchedUpdates
包裹回调 → 批量更新。 - 但
setTimeout
、Promise.then
属于原生调度,React 无法感知,不会进入batchedUpdates
。
所以在 React17 里:
scss
setTimeout(() => {
setCount(c => c + 1); // render 一次
setCount(c => c + 1); // render 再一次
}, 0);
👉 会渲染两次。
六、React18:自动批量更新
React18 引入 Automatic Batching:
- 不管 React 事件、setTimeout、Promise、async/await...
- 只要在同一个任务里触发的更新 → 自动批量合并。
ini
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
}, 0);
👉 React18 里只渲染一次,最终 state = 2。
需要立即更新时,可以用 flushSync
:
javascript
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1); // 立即更新
});
七、isBatchingUpdates = true 的时机
isBatchingUpdates
决定 React 是否开启批量更新:
- true → setState 只入队,不立即渲染
- false → setState 立即触发渲染
React17
- 合成事件回调、生命周期 → true
- 原生事件 / setTimeout / Promise → false
React18
- 自动批量更新 → 所有场景默认 true
- 除非
flushSync
强制同步
源码简化版:
ini
function batchedUpdates(fn, ...args) {
const prev = isBatchingUpdates;
isBatchingUpdates = true;
try {
fn(...args);
} finally {
isBatchingUpdates = prev;
if (!isBatchingUpdates) flushUpdates();
}
}
✅ 总结
- React 更新由 Update + UpdateQueue 驱动。
- render 阶段统一计算 state,commit 阶段更新 DOM。
- 批量更新合并多个 setState,只渲染一次。
- React17:只有合成事件是批量的,异步回调不是。
- React18:自动批量更新,几乎所有场景都合并。
isBatchingUpdates
是批量开关,18 里几乎全局开启。
一句话概括:
React 更新就是 收集更新 → 按优先级调度 → render 阶段合并计算 → commit 阶段一次性更新 。
React18 之后,批量更新从「合成事件级」升级为「全局级」。