这一节,作者分别介绍了legacy模式和Concurrent模式下的更新流程,首先科普一下 这两种模式。
Legacy 模式和 Concurrent 模式
Legacy 模式是 React 在 18 版本之前使用的传统渲染模式的核心工作机制。这种模式在现代 React 中仍然可用,但正在被更先进的并发模式(Concurrent Mode)所取代。
看下下面的对比,我们今天 主要看下两种模式下 批量更新实现的对比, 那么从何说起呢? 就从 更新入口说起吧
🔄 Legacy 模式 vs 并发模式
| 特性 | Legacy 模式 | 并发模式 (Concurrent Mode) |
|-------------------|--------------------------------|-----------------------------------|
| 渲染方式 | 同步不可中断 | 可中断异步渲染 |
| 任务优先级 | 无优先级概念 | 基于任务优先级调度 |
| 批量更新范围 | 仅 React 事件处理器内 | 所有更新(包括异步) |
| Suspense 支持 | 有限支持 | 完全支持 |
| API 入口 | ReactDOM.render()
| ReactDOM.createRoot().render()
|
| 阻塞主线程风险 | 高(长列表/复杂组件) | 低(时间切片)
更新入口 scheduleUpdateOnFiber
还记得入口函数 scheduleUpdateOnFiber 吗?不管你是初次渲染还是后续更新,都会从 这个入口开始渲染流程,我们先看下它的大致逻辑
js
export function scheduleUpdateOnFiber(fiber,lane,eventTime){
/* 递归向上标记更新优先级 */
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (root = = = null) {
return null;
}
if (lane = = = SyncLane) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext && // unbatch 情况,比
如初始化
(executionContext & (RenderContext | CommitContext)) === NoContext) {
/* 开始同步更新,进入 workloop 流程 */
performSyncWorkOnRoot(root);
} else {
/* 进入调度,把任务放入调度中 */
ensureRootIsScheduled(root, eventTime);
if (executionContext === NoContext) {
/* 当前的执行任务类型为 NoContext,说明当前任务是非可控的,会调用 flushSyncCallbackQueue 方法。*/
flushSyncCallbackQueue();
}
}
}
}
请问:对于 react state的批量更新,会走哪个分支呢?
回顾10.1我们知道会走 else 分支,因为Legacy模式下(如下面代码), state 的批量更新,会设置 executionContext 为 批量更新上下文,而非 NoContext,所以 会进入else分支
js
function batchedEventUpdates(){
var prevExecutionContext = executionContext;
executionContext |= EventContext; // 运算赋值
try {
return fn(a); // 执行函数
}finally {.
executionContext = prevExecutionContext; // 重置之前的状态
if (executionContext = = = NoContext) {
flushSyncCallbacksOnlyInLegacyMode() // 同步执行更新队列中的任务 }
}
}
}
export function flushSyncCallbacksOnlyInLegacyMode(){
if(includesLegacySyncCallbacks){
/*只有在legacy模式下,才会走这里的流程。*/
flushSyncCallbacks();
}
}
接下来就到了批量更新的关键函数 ensureRootIsScheduled
legacy 模式更新流程
ensureRootIsScheduled
首先看下它的实现:
js
function ensureRootIsScheduled(root,currentTime){
/* 计算一下执行更新的优先级 */
var newCallbackPriority = returnNextLanesPriority();
/* 当前 Root 上存在的更新优先级 */
const existingCallbackPriority = root.callbackPriority;
/* 如果两者相等,说明是在一次更新中,那么将退出 */
if(existingCallbackPriority = = = newCallbackPriority){
return
}
if (newCallbackPriority = = = SyncLanePriority) {
/* 在正常情况下,会直接进入调度任务中。*/
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}else{
/* 这里先忽略 */
}
/* 将当前 Root 的更新优先级,绑定到最新的优先级 */
root.callbackPriority = newCallbackPriority;
}
我们来通俗的讲下这个流程:
javascript
function handleClick() {
setCount(1); // 第一次:调度渲染
setCount(2); // 第二次:优先级相同,跳过调度
setCount(3); // 第三次:优先级相同,跳过调度
// 最终只会有一次渲染
}
当第一次调用setState时:
-
计算
newCallbackPriority
(假设为优先级X) -
此时
existingCallbackPriority
为NoPriority(初始值) -
两者不相等 → 进入调度流程
-
调度完成后设置
root.callbackPriority = X
-
当在同一个事件循环中再次调用setState:
-
计算新的
newCallbackPriority
(仍然是X,因为相同类型的更新) -
此时
existingCallbackPriority
已经是X -
两者相等 → 直接退出,不再调度
结果:同一个事件循环中的多次setState只会触发一次调度(即一次渲染)
我的小疑问
看到这里的时候我其实有个小疑问
批量更新,跳过调度,那怎么拿到最新的状态呢?
其实,在React中,批量更新并不会跳过状态更新,而是跳过重复的调度任务。所有状态更新都会被收集起来,在同一个调度任务中处理。
让我们深入理解这个过程:
- 状态更新流程:
-
当调用setState或useState时,会创建一个更新对象,并添加到对应Fiber节点的更新队列中
-
触发调度请求(通过ensureRootIsScheduled)
-
调度系统会安排一个任务来处理所有积累的更新
- 跳过调度的含义:
-
跳过的是重复创建调度任务的过程,不是跳过状态更新
-
当多次setState时,只有第一次会创建调度任务
-
后续setState只会将更新添加到队列,而不会创建新任务
- 关键代码流程:
进入调度流程 scheduleSyncCallback
js
function scheduleSyncCallback(callback) {
if (syncQueue = = = null) {
/* 如果队列为空 */
syncQueue = [callback];
/* 放入调度任务 */
immediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl);
}else{
/* 如果任务队列不为空,那么将任务放入队列中。*/
syncQueue.push(callback);
}
}
- flushSyncCallbackQueueImpl 会真正执行 callback,本质上就是调和函数 performSyncWorkOnRoot。
- Scheduler_scheduleCallback 就是在调度章节讲的调度的执行方法,就是通过 MessageChannel 向浏览器 请求下一个空闲帧,在空闲帧中执行更新任务。
scheduleSyncCallback 做的事情如下:
- 如果执行队列为空,那么把当前任务放入队列中,然后执行调度任务。
- 如果队列不为空,此时已经在调度中,那么不需要执行调度任务,只需要把当前更新放入队列中即可,调度中心会逐一按照顺序执行更新任务。
讲到这,作者提到了一个问题:
比如在浏览器空闲状态下发生一次 state 更新,那么最后一定会进入调度,等到下一次空闲帧执行吗?
答案是否定的,如果这样,那么就是一种性能的浪费,因为正常情况下,发生更新希望的是在第一次事件循环中执行完更新并且渲染视图,如果在下一次事件循环中执行,那么更新肯定会延时。
React 是如何处理这个情况的呢?
空闲期的同步任务 -- 澄清
虽然没有提供源码,但是作者在这里做出了澄清:
在没有更新任务空闲期的条件下,为了让更新变成同步的,也就是本次更新不在调度中执行, React 对于更新,会用 flushSyncCallbackQueue 立即执行更新队列,发起更新任务,目的就是让任务不延时到下一帧。但是此时调度会正常执行,不过调度中的任务已经被清空。
有的同学可能会产生疑问,既然不让任务进入调度,而选择同步执行任务,那么调度的意义是什么呢?
调度的目的是处理存在多个更新任务的情况,比如发生了短时间内连续的点击事件,每次点击事件都会更新 state,那么对于这种更新并发的情况,第一个任务以同步任务执行,接下来的任务将放入调度,等到调度完成后,在下一个空闲帧时执行。
同步更新流程
我们通过一个例子,总结一下更新流程
js
const [number, setNumber ]= React.useState(0)
const handleClick = () =>{ /* 同步条件下 */
setNumber(1)
setNumber(2)
}
return <div>
{number}
<button onClick ={handleClick} >点击</button>
</div>
}
当点击按钮的时候,会触发两次 setNumber,那么这两次 setNumber 都做了些什么呢?
整个流程如下:
- 事件上下文:开启事件开关 →进入第一次 setNumber。
- 第一次 setNumber 上下文:scheduleUpdateOnFiber → ensureRootIsScheduled → scheduleSyncCallback (放入回调函数 performSyncWorkOnRoot)。
- 第二次 setNumber 上下文:scheduleUpdateOnFiber → ensureRootIsScheduled (
优先级一致
)→ 退出。 - 事件上下文:关闭事件开关 → flushSyncCallbackQueue。
- flushSyncCallbackQueue →执行回调函数 performSyncWorkOnRoot →进入调和阶段→ renderRoot → commitRoot →浏览器绘制。
异步更新流程
js
const handleClick = () =>{
setTimeout(() => { /* 异步条件下 */
setNumber(1)
setNumber(2)
},0)
}
还记得 调用 ensureRootIsScheduled之后还有什么逻辑吗?
js
if (executionContext === NoContext) {
/* 当前的执行任务类型为 NoContext,说明当前任务是非可控的,会调用 flushSyncCallbackQueue 方法。*/
flushSyncCallbackQueue();
}
在 setTimeout 中的更新,变成了异步更新。
事件上下文:开启事件开关 →关闭事件开关 → flushSyncCallbackQueue (此时更新队列为空)。
setTimeout 上下文:执行第一次 setNumber。
第一次 setNumber 上下文:scheduleUpdateOnFiber → ensureRootIsScheduled → scheduleSyncCallback(放 入 回 调 函 数 performSyncWorkOnRoot)→ flushSyncCallbackQueue
→ 同步
执 行 回 调 函 数 performSyncWorkOnRoot →进入调和阶段→ renderRoot → commitRoot。(我理解此时优先级已经reset
)
回到 setTimeout 上下文:执行第二次 setNumber。
第二次 setNumber 上下文:scheduleUpdateOnFiber → ensureRootIsScheduled → scheduleSyncCallback(放 入 回 调 函 数 performSyncWorkOnRoot)→ flushSyncCallbackQueue
→ 同步
执 行 回 调 函 数 performSyncWorkOnRoot →进入调和阶段→ renderRoot → commitRoot。
JS 执行完毕,浏览器进行绘制。这种情况下渲染了两遍。到此为止,legacy 模式下更新流程真相大白了。
Concurrent 模式更新流程
ensureRootIsScheduled
新版 React:
js
function ensureRootIsScheduled(root,currentTime){
var existingCallbackNode = root.callbackNode;
var newCallbackPriority = getHighestPriorityLane(nextLanes);
var existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority = = = newCallbackPriority &&
! (ReactCurrentActQueue.current! = = null && existingCallbackNode !== fakeActCallbackNode)) {
/* 批量更新退出* */
return;
}
/* 同步更新条件下,会走这里的逻辑 */
if (newCallbackPriority = = = SyncLane) {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
/* 用微任务去立即执行更新 */
scheduleMicrotask(flushSyncCallbacks);
}else{
var schedulerPriorityLevel
// ...计算当前的优先级 /* 不是同步的情况下,会走这里 */
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
/* 这里很重要就是给当前 Root 赋予 callbackPriority 和 callbackNode 状态 */
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
我们再跟旧版react对比一下:
js
function ensureRootIsScheduled(root,currentTime){
/* 计算一下执行更新的优先级 */
var newCallbackPriority = returnNextLanesPriority();
/* 当前 Root 上存在的更新优先级 */
const existingCallbackPriority = root.callbackPriority;
/* 如果两者相等,说明是在一次更新中,那么将退出 */
if(existingCallbackPriority = = = newCallbackPriority){
return
}
if (newCallbackPriority = = = SyncLanePriority) {
/* 在正常情况下,会直接进入调度任务中。*/
newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}else{
/* 这里先忽略 */
}
/* 将当前 Root 的更新优先级,绑定到最新的优先级 */
root.callbackPriority = newCallbackPriority;
}
其实核心逻辑很像,都是通过更新优先级来判断是否创建新的更新任务
同步步条件下的逻辑
- 同步更新流程(当
newCallbackPriority === SyncLane
时):
-
第一次调用
useState
时: -
计算
newCallbackPriority
(此时为SyncLane
) -
由于是首次更新,
existingCallbackPriority
(即root.callbackPriority
)与新的优先级不同 -
进入同步更新分支:
-
调用
scheduleSyncCallback
:将任务(performSyncWorkOnRoot
)推入syncQueue
队列 -
调用
scheduleMicrotask
:安排一个微任务来执行flushSyncCallbacks
(该函数会清空syncQueue
)--注意这里是新旧版的差别哦!!!
-
更新
root.callbackPriority
和root.callbackNode
为当前任务的信息 -
任务将在微任务中执行(即当前JS执行栈清空后立即执行)
- 第二次调用
useState
(同步更新中):
-
再次进入
ensureRootIsScheduled
,计算newCallbackPriority
(仍为SyncLane
) -
此时
existingCallbackPriority
(即前一次设置的优先级)等于newCallbackPriority
-
满足条件:
existingCallbackPriority === newCallbackPriority
,直接退出函数 -
结果:第二次更新不会产生新的调度任务,但它的更新会被收集到同一个同步队列中
异步条件下的逻辑
这里注意一点: 在并发模式(ReactDOM.createRoot)下,非可控更新(如setTimeout中的更新)会被赋予默认优先级(不是SyncLane),因此进入else分支,使用
scheduleCallback(即Scheduler中的调度方法)进行调度,从而实现并发渲染。
最后放上一张图,你看懂了吗?

新老react的批量更新原理我们基本都讲完了,这节不知道为什么看了很多遍,才看懂一些,我们下一节见吧!