前言:为什么要读 React 源码?
当我们调用 setCount 时,React 底层究竟发生了什么?为什么有时候页面丝滑无比,有时候却卡顿掉帧?为什么 useEffect 的清理函数总是在奇怪的时候执行?
想要彻底搞懂这些玄学问题,就必须深入 React 的核心 Fiber 架构。React 的核心进化动力是: “将同步的递归过程,变为可中断的异步循环” 。为了实现这一点,它将渲染链路拆解为了三个各司其职的核心模块。
我们可以把 React 的运行比作一个高效的工厂生产线:
- Scheduler (调度器) —— 调度员:维护任务优先级,利用“时间切片”在浏览器空闲时触发回调,防止长任务阻塞主线程。
- Reconciler (协调器) —— 施工队:负责 Diff 算法。在内存中构建 Fiber 树,打上增删改的标记(Flags)。这个过程是可中断的。
- Renderer (渲染器) —— 装修工:负责把计算好的变更一次性同步到宿主环境(如 DOM)。这个过程是不可中断的。
前前言:源码准备
为了下一篇的实践篇做准备,建议不要直接 clone 官方的 facebook/react 仓库。因为官方仓库使用了复杂的 Monorepo 结构、Flow 类型检查(部分切 TypeScript)以及自定义的打包配置。
我们只需要看最核心的模块就可以了,于是推荐查看 node_modules 下的React源码,在合适文件夹处打开终端并输入以下命令。
npx create-react-app react-source-debug
cd react-source-debug
npm start
为了方便追踪更新流程,把 src/App.js 改成一个最简单的计数器:
import { useState } from "react";
function App() {
const [count, setCount] = useState(0);
const click = () => {
setCount(count + 1);
};
return (
<div className="App">
<h1>调试代码</h1>
<p>{count}</p>
<button onClick={click}>加 1</button>
</div>
);
}
export default App;
主包的版本如下:
"dependencies": {
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^13.5.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
第一站:Scheduler (调度器) —— 掌控时间的艺术
如果你的组件树极其庞大,React 一次性计算所有的差异(Diff)可能会花费几百毫秒,导致主线程卡死。Scheduler 的作用就是 “时间切片” (Time Slicing)。
1. 双队列设计
当你触发更新时,Scheduler 内部会将任务分发到两个最小堆(队列)中:
timerQueue:未到时间的延迟任务(候补区)。taskQueue:已到时间的就绪任务(核心工作区)。 系统会通过定时器将到期的任务从timerQueue移入taskQueue,并开始执行工作循环 (workLoop)。
2. 时间切片与 MessageChannel
React 默认的时间切片是 5ms。如果一个任务执行超过了 5ms,React 就会通过 shouldYieldToHost 中断执行,把主线程还给浏览器去渲染 UI。
接下来让我们从入口开始一步步阅读到底Scheduler做了哪些事情:
exports.unstable_scheduleCallback = function (
priorityLevel,
callback,
options
) {
// 1. 计算任务的“开始时间” (startTime)
// 判断开发者有没有传入 delay(延迟执行时间)。
// 如果有 delay,任务的开始时间就是 当前时间 + delay。
// 如果没有,开始时间就是 当前时间。
var currentTime = exports.unstable_now(); // 相当于performance.now(),获取时间戳,用来代码执行时间分片
if ("object" === typeof options && null !== options) {
options = options.delay; // 变量复用 options 从配置对象变成了 delay 数值
if (typeof options === "number" && options > 0) {
options = currentTime + options; // options 变成了 startTime
} else {
options = currentTime;
}
} else {
options = currentTime;
}
// 2. 计算任务的“过期时间” (expirationTime)
// 根据权重来计算时间片,默认为5000us(微秒),也就是5ms(毫秒)
// Scheduler 内部定义了 5 种优先级。优先级越高,timeout 越小,意味着任务越容易“过期”。
// 一旦任务过期,即使破坏帧率,React 也会强制同步执行它。
switch (priorityLevel) {
case 1: var timeout = -1; break; // Immediate (立即执行,已经过期了)
case 2: timeout = 250; break; // UserBlocking (用户阻塞级,如点击事件)
case 5: timeout = 1073741823; break; // Idle (空闲,相当于永不过期)
case 4: timeout = 1e4; break; // Low (低优先级)
default: timeout = 5e3; // Normal (普通优先级,默认 5ms)
}
timeout = options + timeout; // timeout 变成了 expirationTime (开始时间 + 超时时间)
// 3. 组装“任务对象” (Task Object)
priorityLevel = { // 变量复用
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: options, // options真实语义:startTime
expirationTime: timeout, // timeout真实语义:expirationTime
sortIndex: -1
};
// 4. 双队列分发与唤醒
// 两个小根堆:timerQueue:未到时间的任务(候补区)。taskQueue:已到时间的就绪任务(核心工作区)。
// 逻辑like:如果你预约的是明天的号,就先去 timerQueue 呆着,系统会定个闹钟;如果你挂的是今天的号,就直接进 taskQueue,并立刻呼叫医生(唤醒 workLoop)开始接诊。
if (options > currentTime) {
// // 场景 A:startTime > currentTime,说明这是一个【延迟任务】
priorityLevel.sortIndex = options; // 延迟任务按照 startTime 排序
push(timerQueue, priorityLevel); // 放进专门存放延迟任务的 timerQueue
// 如果当前没有就绪任务,且这是当前任务在堆顶,也就是最先需要唤醒的延迟任务
if (peek(taskQueue) === null && priorityLevel === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
localClearTimeout(taskTimeoutID);
taskTimeoutID = -1;
} else {
isHostTimeoutScheduled = true;
}
// 设置一个定时器 (setTimeout),到点后把它移到 taskQueue 里
requestHostTimeout(handleTimeout, options - currentTime);
}
} else {
// 场景 B:这是一个【立即需要执行的任务】
priorityLevel.sortIndex = timeout; // 就绪任务按照 expirationTime 排序
push(taskQueue, priorityLevel); // 放进就绪队列 taskQueue
// 如果现在没有在执行任务,就通知宿主环境(浏览器)启动宏任务开始干活!
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
}
return priorityLevel;
};
当你点击页面上 加 1,调用 setCount 的时候,Scheduler会先包装该任务,入队 taskQueue 队列。
接下来看看最后的函数 schedulePerformWorkUntilDeadline 做了什么,查找该定义发现其实该函数做了一些 polyfill 降级处理:
// schedulePerformWorkUntilDeadline降级
// 1. 首选:setImmediate (主要针对 Node.js 和 老版 IE)
if ("function" === typeof localSetImmediate)
var schedulePerformWorkUntilDeadline = function () {
localSetImmediate(performWorkUntilDeadline);
};
// 2. 备选:MessageChannel (绝大部分现代浏览器,如 Chrome, Safari, Firefox)
// MessageChannel 是 HTML5 引入的一个 API,允许我们创建一个新的消息通道,包含两个 MessagePort 对象。
// 我们可以在任意一个 port 上发送消息,而另一个 port 则会触发 onmessage 事件。
// 这使得 MessageChannel 成为一种非常高效的方式来安排任务在浏览器的事件循环中执行。
else if ("undefined" !== typeof MessageChannel) {
var channel = new MessageChannel(),
port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline; // 真正执行调度任务
schedulePerformWorkUntilDeadline = function () {
port.postMessage(null); // 发送消息,触发宏任务
};
} else // 3. 兜底方案:setTimeout
schedulePerformWorkUntilDeadline = function () {
localSetTimeout(performWorkUntilDeadline, 0);
};
原理解析:为什么是 MessageChannel?
React 的“时间切片”本质上就是:干 5ms 活 -> 停下来 -> 让浏览器渲染 UI / 响应点击 -> 再干 5ms 活。
为了实现“停下来再继续”,React 必须把剩下的活儿放到浏览器的事件循环 (Event Loop) 中排队。
React 为什么不用
setTimeout(fn, 0)?因为在浏览器嵌套调用时,setTimeout会有至少 4ms 的强制最小延迟,这会严重浪费性能。React 为什么不用
requestIdleCallback?因为这个 API 的触发频率极不稳定,有时候一秒钟才触发一两次,在 Safari 上还不支持。所以,React 选择了
MessageChannel。它是一个纯粹的宏任务(MacroTask) ,没有哪怕 1ms 的人为延迟,非常适合用来实现高频的任务调度。
可以看到 schedulePerformWorkUntilDeadline 最后通过 MessageChannel 调用了 performWorkUntilDeadline。
function performWorkUntilDeadline() {
needsPaint = false; // 每次工作前,先重置紧急绘制标志
if (isMessageLoopRunning) {
var currentTime = exports.unstable_now();
startTime = currentTime; // 记录当前宏任务的开始时间 (用于给 shouldYieldToHost 计算 5ms)
var hasMoreWork = true;
try {
a: {
isHostCallbackScheduled = !1;
isHostTimeoutScheduled &&
((isHostTimeoutScheduled = !1),
localClearTimeout(taskTimeoutID),
(taskTimeoutID = -1));
isPerformingWork = !0;
var previousPriorityLevel = currentPriorityLevel;
try {
b: {
// 1. 把 timerQueue 里到期的任务挪到 taskQueue 里
advanceTimers(currentTime);
// 2. 核心循环 workLoop
for (
currentTask = peek(taskQueue); // taskQueue里取出任务
null !== currentTask &&
!( // 这里任务过期和不需要让出主线程是一个或的关系,满足其一就继续执行任务
currentTask.expirationTime > currentTime && // 任务已过期,也就是到了执行时间
shouldYieldToHost() // 不需要让出主线程
);
) {
var callback = currentTask.callback;
if ("function" === typeof callback) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
var continuationCallback = callback( // 【真正执行任务的地方】比如去执行 Reconciler 构建 Fiber 树
currentTask.expirationTime <= currentTime
);
currentTime = exports.unstable_now();
if ("function" === typeof continuationCallback) { // 如果任务执行完返回了一个函数,说明任务没干完被中断了
currentTask.callback = continuationCallback;
advanceTimers(currentTime);
hasMoreWork = !0;
break b; // 跳出循环,准备让出控制权
}
currentTask === peek(taskQueue) && pop(taskQueue); // 出队列
advanceTimers(currentTime); // 再次排序
} else pop(taskQueue); // 当前任务callback不是函数,直接出队列
currentTask = peek(taskQueue); // 再次取出堆顶
}
if (null !== currentTask) hasMoreWork = !0;
else {
var firstTimer = peek(timerQueue);
null !== firstTimer &&
requestHostTimeout(
handleTimeout,
firstTimer.startTime - currentTime
);
hasMoreWork = !1;
}
}
break a;
} finally {
(currentTask = null),
(currentPriorityLevel = previousPriorityLevel),
(isPerformingWork = !1); // 结束工作,重置状态
}
hasMoreWork = void 0;
}
} finally {
// 如果活没干完,再发一个宏任务消息,重新排队
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = !1;
}
}
}
}
到底是怎么“把控制权交给浏览器”的?
首先看看如何判断是不是需要让出主线程呢:
function shouldYieldToHost() {
return needsPaint
? !0 // 翻译:如果 needsPaint 是 true,立刻返回 true (让出主线程)
: exports.unstable_now() - startTime < frameInterval
? !1 // 翻译:如果没有到 5ms,返回 false (继续干活)
: !0; // 翻译:如果超过了 5ms,返回 true (让出主线程)
}
很多人觉得 shouldYieldToHost() 像魔法一样,以为调用它就能“暂停”代码运行。其实在普通的 JavaScript 里,没有魔法,函数一旦执行就必须运行到底(Run-to-completion)。
React 的“交出控制权”其实是一个主动退出的策略,它利用了浏览器的 Event Loop(事件循环)机制:
- 打断施法:当
shouldYieldToHost()返回true时,React 只是简单地执行了break,跳出了那个巨大的for循环。 - 安排后事:循环跳出后,走到最后的
finally块。因为发现还有任务没做完(hasMoreWork = true),于是调用schedulePerformWorkUntilDeadline()。注意,这并不是递归调用!这是往MessageChannel里发一条消息,在宏任务队列的末尾重新排队。 - 彻底结束:发完消息后,
performWorkUntilDeadline这个函数就执行完了,调用栈(Call Stack)清空。 - 浏览器接管:此时当前宏任务结束。浏览器的 Event Loop 一看,调用栈空了!于是它开心地去处理用户的点击事件、去计算 CSS、去重新渲染页面(Paint)。
- 再次循环:浏览器把自己的活儿干完后,一看宏任务队列,发现刚才 React 排队的消息又到跟前了,于是再次触发
performWorkUntilDeadline,接着干上一轮没干完的活儿。
到这里 Scheduler 的秘密已经揭晓了,其实就是通过任务优先级往两个小根堆里添加任务。当浏览器空闲时,就会取出任务并执行,期间也会判断是否需要让出主线程,也可以被页面绘制任务打断。
完整版本源码解释看文章结尾。
第二站:Reconciler (协调器) —— 内存中的 Fiber 施工队
当 Scheduler 分配好时间后,真正干活的就是 Reconciler。它把原本不可中断的“递归” Diff,变成了带有状态指针 (workInProgress) 的 while 循环。
1. “递”与“归”的艺术
React 构建 Fiber 树的过程,本质上就是一个深度优先遍历 (DFS) ,但是 React 把原本不可中断的“递归”函数调用,巧妙地改写成了带有状态指针 (workInProgress) 的 while/for 循环。
整个过程分为两个极其重要的阶段:
-
beginWork(递 - 向下挖掘) :- 顾名思义就是“开始工作”。它会对比新旧节点(Diff 算法的核心发源地),计算出需要更新的属性,并创建下一个子节点。
- 如果它返回了子节点,
workInProgress就会一直往下指,直到某个节点没有子节点为止。
-
completeUnitOfWork(归 - 向上收尾) :-
当
beginWork走到叶子节点(返回null)时,就会触发completeUnitOfWork。 -
这个函数内部会负责在这个节点上“收集副作用”(比如给 DOM 打上增删改的 Tag),并尝试构建真实的 DOM 节点结构(在内存中) 。
-
关键寻路逻辑:在
completeUnitOfWork内部,它做完当前节点的活儿之后,会先看看自己有没有兄弟节点 (sibling) 。- 如果有兄弟节点,它就把
workInProgress扔给兄弟,让兄弟去执行beginWork。 - 如果没有兄弟节点,说明这一层的节点都处理完了,它就退回到父节点 (
return) ,继续执行父节点的completeUnitOfWork。
- 如果有兄弟节点,它就把
-
在内存里创建真实的 DOM 节点时,会巧妙地向上冒泡收集
subtreeFlags。
-
核心入口 workLoopSync,有两种模式,这种是同步模式,并发模式下核心入口为 workLoopConcurrentByScheduler:
function workLoopConcurrentByScheduler() {
// 相较于workLoopSync,多了是否让出主线程的判断,其实就是Scheduler里的判断
for (; null !== workInProgress && !shouldYield(); )
performUnitOfWork(workInProgress);
}
// 处理单个 Fiber 节点
function performUnitOfWork(unitOfWork) {
var current = unitOfWork.alternate; // 获取当前节点在旧树上对应的节点(双缓存机制)
// 【递阶段】:调用 beginWork,向下构建节点,并返回下一个要处理的子节点
if ((unitOfWork.mode & 2) !== NoMode) {
startProfilerTimer(unitOfWork);
current = runWithFiberInDEV(
unitOfWork,
beginWork, // 对比新旧节点(Diff 算法的核心发源地),计算出需要更新的属性,并创建下一个子节点。
current,
unitOfWork,
entangledRenderLanes
);
stopProfilerTimerIfRunningAndRecordDuration(unitOfWork);
} else {
current = runWithFiberInDEV(
unitOfWork,
beginWork,
current,
unitOfWork,
entangledRenderLanes
);
}
// 记录已经处理完的 props
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (null === current) {
// 【归阶段】:如果没有子节点了,说明当前分支走到底了(叶子节点),开始往回走
// 负责在这个节点上“收集副作用”(比如给 DOM 打上增删改的 Tag),并尝试构建真实的 DOM 节点结构(在内存中)
completeUnitOfWork(unitOfWork);
} else {
// 如果还有子节点,把全局指针 workInProgress 指向它,下次循环继续处理它
workInProgress = current;
}
}
假设我们有如下非常简单的组件树:
<App>
<Parent>
<ChildA />
<ChildB />
</Parent>
</App>
当 React 开始构建这棵树,workInProgress 指针初始指向 <App>:
performUnitOfWork执行,调用<App>的beginWork,返回了<Parent>。workInProgress移动到<Parent>。- 再次循环,调用
<Parent>的beginWork,返回了<ChildA>。 workInProgress移动到<ChildA>。<ChildA>没有子节点,它会返回null。- 然后走到
completeUnitOfWork(<ChildA>),之后wip指针指向<ChildA>的sibling节点,也就是<ChildB> - 下一把开始进行
<ChildB>的beginWork
function beginWork(current, workInProgress, renderLanes) {
// 1. 热替换(Hot Reload)特殊处理:_debugNeedsRemount 强制替换整个 Fiber
if (workInProgress._debugNeedsRemount && null !== current) {
// ... 创建新 Fiber,替代当前 workInProgress,并标记删除旧节点
return renderLanes; // 返回新创建的 Fiber
}
// 2. 判断是否能直接复用当前节点(Bailout 优化)
if (null !== current) {
// 对比 props 和 type 是否变化
if (current.memoizedProps !== workInProgress.pendingProps ||
workInProgress.type !== current.type) {
didReceiveUpdate = true;
} else {
// props 和 type 没变,检查是否有待处理的更新或副作用
if (!checkScheduledUpdateOrContext(current, renderLanes) &&
(workInProgress.flags & 128) === 0) {
didReceiveUpdate = false;
// 尝试提前退出(bailout),复用子树
return attemptEarlyBailoutIfNoScheduledUpdate(current, workInProgress, renderLanes);
}
didReceiveUpdate = (current.flags & 131072) !== 0;
}
} else {
didReceiveUpdate = false;
// 首次渲染时的 hydration 相关处理(服务端渲染)
// ...
}
// 3. 清除当前 Fiber 上暂存的 lanes(即将进入具体处理)
workInProgress.lanes = 0;
// 4. 根据 Fiber 的 tag 类型,分发到不同的具体处理函数
switch (workInProgress.tag) {
case 0: return updateFunctionComponent(current, workInProgress, ...);
case 1: return updateClassComponent(current, workInProgress, ...);
case 3: return updateHostRoot(current, workInProgress, ...); // 根节点
case 5: return updateHostComponent(current, workInProgress, ...); // DOM 元素
case 6: return updateHostText(current, workInProgress); // 文本节点
case 13: return updateSuspenseComponent(current, workInProgress, renderLanes);
// ... 其他 tag(forwardRef, memo, context provider/consumer, offscreen 等)
default: throw new Error("Unknown unit of work tag");
}
}
我们再来看看 completeUnitOfWork
function completeUnitOfWork(unitOfWork) {
var completedWork = unitOfWork; // 当前要完成的节点
do {
// 1. 检查是否需要异常回退(Incomplete 标志)
if (0 !== (completedWork.flags & 32768)) {
unwindUnitOfWork(completedWork, workInProgressRootDidSkipSuspendedSiblings);
return;
}
var current = completedWork.alternate; // 旧树中的对应节点
var returnFiber = completedWork.return; // 父节点
startProfilerTimer(completedWork); // 性能计时
// 2. 调用 completeWork 完成节点工作
current = runWithFiberInDEV(
completedWork,
completeWork,
current,
completedWork,
entangledRenderLanes
);
// 性能计时结束
if ((completedWork.mode & 2) !== NoMode) {
stopProfilerTimerIfRunningAndRecordIncompleteDuration(completedWork);
}
// 3. 如果 completeWork 返回了新的节点(罕见),则继续向下处理
if (null !== current) {
workInProgress = current;
return;
}
// 4. 尝试处理兄弟节点
var siblingFiber = completedWork.sibling;
if (null !== siblingFiber) {
workInProgress = siblingFiber;
return;
}
// 5. 没有兄弟节点,则向上回溯到父节点
workInProgress = completedWork = returnFiber;
} while (null !== completedWork); // 一直回溯到根节点
// 6. 整个根节点树已完成
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
2. 精准打击的秘密
我们可以看到在completeUnitOfWork调用了completeWork,其实completeWork 是 React Fiber 协调器中针对单个节点执行“完成”操作的函数,而 completeUnitOfWork 是驱动整个后序(归)阶段遍历的调度循环。二者配合完成渲染阶段的“归”过程。
对于传入的一个 workInProgress 节点(正在构建的新 Fiber),completeWork 根据其 tag(组件类型)执行对应的收尾工作,主要包括:
- 创建或更新 DOM 实例(对宿主组件
tag=5、文本节点tag=6等) - 将子节点附加到父节点(如
appendAllChildren) - 处理 Hydration(服务端渲染水合) :验证并复用已有的 DOM 节点
- 冒泡副作用(
bubbleProperties) :将子树的 flags(如 Placement、Update)合并到当前 Fiber 的subtreeFlags中,方便 commit 阶段快速查找 - 处理 Suspense、Offscreen 等特殊组件的逻辑(如显示 fallback、标记隐藏)
- 弹出上下文(如 Provider、HostContext、SuspenseContext)
而冒泡副作用(bubbleProperties) 让React可以在Commit阶段跳过没有副作用的子树,极大的提升了性能。
function bubbleProperties(completedWork) {
// 1. 判断当前节点是否“保退”(bailout)了
var didBailout =
null !== completedWork.alternate &&
completedWork.alternate.child === completedWork.child;
// 如果 current 树和 workInProgress 树的 child 指向同一个 Fiber 节点,
// 说明当前节点没有重新构建子树(复用了旧子节点)。
// 此时冒泡时只能合并某些特定的 flags(如悬浮、挂起相关),不能完全合并所有 flags。
var newChildLanes = 0, // 用于汇总子节点的 lanes 和 childLanes
subtreeFlags = 0; // 用于汇总子节点的 flags 和 subtreeFlags
if (didBailout) {
// 情况 A:当前节点复用了旧的子节点(没有重新协调子树)
if ((completedWork.mode & 2) !== NoMode) {
// 性能追踪模式(ProfileMode):还要累加时长
var _treeBaseDuration = completedWork.selfBaseDuration,
_child2 = completedWork.child;
while (_child2 !== null) {
// 合并子节点的 lanes
newChildLanes |= _child2.lanes | _child2.childLanes;
// 注意:这里只合并了部分 flags(0x65011712 是一个掩码,代表某些特定 flags)
// 因为在 bailout 时,子树的某些 flags 不能简单向上冒泡(需要特殊处理)
subtreeFlags |= _child2.subtreeFlags & 65011712;
subtreeFlags |= _child2.flags & 65011712;
// 累加基础时长(用于性能分析)
_treeBaseDuration += _child2.treeBaseDuration;
_child2 = _child2.sibling;
}
completedWork.treeBaseDuration = _treeBaseDuration;
} else {
// 非性能追踪模式,只合并 lanes 和部分 flags,不累加时长
for (var _child = completedWork.child; _child !== null; _child = _child.sibling) {
newChildLanes |= _child.lanes | _child.childLanes;
subtreeFlags |= _child.subtreeFlags & 65011712;
subtreeFlags |= _child.flags & 65011712;
_child.return = completedWork; // 确保父指针正确
}
}
} else {
// 情况 B:当前节点是全新构建的(或至少没有完全复用旧子节点)
if ((completedWork.mode & 2) !== NoMode) {
// 性能追踪模式:完整合并所有 flags 和时长
var _actualDuration = completedWork.actualDuration;
var _treeBaseDuration2 = completedWork.selfBaseDuration;
for (var _child3 = completedWork.child; _child3 !== null; _child3 = _child3.sibling) {
newChildLanes |= _child3.lanes | _child3.childLanes;
subtreeFlags |= _child3.subtreeFlags;
subtreeFlags |= _child3.flags;
_actualDuration += _child3.actualDuration;
_treeBaseDuration2 += _child3.treeBaseDuration;
}
completedWork.actualDuration = _actualDuration;
completedWork.treeBaseDuration = _treeBaseDuration2;
} else {
// 非性能追踪模式:完整合并所有 flags
for (var _child4 = completedWork.child; _child4 !== null; _child4 = _child4.sibling) {
newChildLanes |= _child4.lanes | _child4.childLanes;
subtreeFlags |= _child4.subtreeFlags;
subtreeFlags |= _child4.flags;
_child4.return = completedWork;
}
}
}
// 2. 将合并后的子节点标志挂到当前节点上
completedWork.subtreeFlags |= subtreeFlags;
completedWork.childLanes = newChildLanes;
// 3. 返回是否发生了 bailout(供上层可能使用)
return didBailout;
}
下面用一个例子来理解冒泡副作用: 假设一个组件树:
Root
├─ App
│ ├─ Header (无更新)
│ └─ Content (需要更新)
- 在
completeWork阶段,Header的subtreeFlags为 0,Content的flags包含Update,subtreeFlags可能也包含子孙的 flags。 bubbleProperties在App节点上会将Content的flags和subtreeFlags冒泡到App.subtreeFlags。- commit 阶段从
Root开始,看到Root.subtreeFlags非 0,进入App;看到App.subtreeFlags非 0,再进入Header和Content;Header.subtreeFlags为 0,跳过;Content.flags非 0,处理更新。 - 如果没有
bubbleProperties,commit 阶段每次都要遍历所有节点,性能会差很多。
3. Bailout (急救退出) 机制
如果在 beginWork 时发现 oldProps === newProps 且 state 没变,React 会直接克隆旧的 Fiber 子节点,并 return null 阻断当前分支的向下遍历。这就是 React.memo 提升性能的底层原理。
第三站:Renderer (渲染器) —— 雷厉风行的装修工
当 Reconciler 在内存中把整棵树建好后,就会进入 Renderer (commitRoot)。 Renderer 的底线是:绝对不可中断! 否则用户就会看到一半新一半旧的“UI 撕裂(Tearing)”。
它严格分为三个同步阶段:
- BeforeMutation:DOM 突变前。
- Mutation:操作真实 DOM,增删改查。
- Layout:真实 DOM 已更新,同步执行
useLayoutEffect。
commitRoot 是 React commit 阶段的总入口。它的任务是将渲染阶段(render phase)生成的 finishedWork 树(即最新的 workInProgress 树)上的副作用(如 DOM 更新、生命周期调用、useEffect 等)同步地应用到真实 UI 上。
function commitRoot(root, finishedWork, lanes, ...) {
// 1. 清空待处理的“被动效果”(useEffect 清理/执行)
do flushPendingEffects(); while (pendingEffectsStatus !== NO_PENDING_EFFECTS);
// 2. 打印 StrictMode 相关警告
ReactStrictModeWarnings.flushLegacyContextWarning();
ReactStrictModeWarnings.flushPendingUnsafeLifecycleWarnings();
// 3. 检查是否已经在工作中,防止嵌套 commit
if ((executionContext & (RenderContext | CommitContext)) !== NoContext)
throw Error("Should not already be working.");
// 4. 记录渲染阶段的各种日志(性能、错误、恢复等)
// (根据 exitStatus 调用不同的 log 函数)
if (exitStatus === RootErrored) { ... }
else if (recoverableErrors) { ... }
else { ... }
// 5. 如果 finishedWork 有效,开始正式提交
if (null !== finishedWork) {
// 5.1 标记根节点为已完成,清理待处理 lanes,更新根节点的 current 指针
markRootFinished(root, lanes, ...);
// 5.2 清空全局 workInProgress 相关变量
root === workInProgressRoot && (workInProgress = workInProgressRoot = null, ...);
// 5.3 将 finishedWork 等信息存到全局 pending 变量,供被动效果使用
pendingFinishedWork = finishedWork;
pendingEffectsRoot = root;
// ...
// 5.4 判断是否有需要触发的“被动效果”(useEffect 的清理或执行)
if (有副作用标记(如 10256 等)) {
// 调度一个普通优先级的回调,稍后执行 flushPassiveEffects
scheduleCallback(NormalPriority$1, function () {
flushPassiveEffects(); // 执行所有 useEffect
return null;
});
} else {
// 没有被动效果,直接清空回调
root.callbackNode = null;
root.callbackPriority = 0;
}
// 5.5 记录 commit 开始时间
commitStartTime = now();
// 5.6 执行 before mutation 阶段(调用 getSnapshotBeforeUpdate 等)
if (finishedWork.subtreeFlags & 13878 || finishedWork.flags & 13878) {
// 保存当前上下文,进入 CommitContext
commitBeforeMutationEffects(root, finishedWork, lanes);
}
// 5.7 标记进入 mutation 阶段,并执行 DOM 突变操作
pendingEffectsStatus = PENDING_MUTATION_PHASE;
flushMutationEffects(); // 执行 DOM 增删改
// 5.8 执行 layout 阶段(同步调用 useLayoutEffect 等)
flushLayoutEffects();
// 5.9 清理可能产生的额外工作
flushSpawnedWork();
}
}
Scheduler完整源码
Renderer和Reconciler源码过于复杂,我们只关注上述核心流程即可。
/**
* @license React
* scheduler.development.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
"use strict";
"production" !== process.env.NODE_ENV &&
(function () {
// 调度任务执行者
function performWorkUntilDeadline() {
needsPaint = false; // 每次工作前,先重置紧急绘制标志
if (isMessageLoopRunning) {
var currentTime = exports.unstable_now();
startTime = currentTime; // 记录当前宏任务的开始时间 (用于给 shouldYieldToHost 计算 5ms)
var hasMoreWork = true;
try {
a: {
isHostCallbackScheduled = !1;
isHostTimeoutScheduled &&
((isHostTimeoutScheduled = !1),
localClearTimeout(taskTimeoutID),
(taskTimeoutID = -1));
isPerformingWork = !0;
var previousPriorityLevel = currentPriorityLevel;
try {
b: {
// 1. 把 timerQueue 里到期的任务挪到 taskQueue 里
advanceTimers(currentTime);
// 2. 核心循环 workLoop
for (
currentTask = peek(taskQueue); // taskQueue里取出任务
null !== currentTask &&
!( // 这里任务过期和不需要让出主线程是一个或的关系,满足其一就继续执行任务
currentTask.expirationTime > currentTime && // 任务已过期,也就是到了执行时间
shouldYieldToHost() // 不需要让出主线程
);
) {
var callback = currentTask.callback;
if ("function" === typeof callback) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
var continuationCallback = callback( // 【真正执行任务的地方】比如去执行 Reconciler 构建 Fiber 树
currentTask.expirationTime <= currentTime
);
currentTime = exports.unstable_now();
if ("function" === typeof continuationCallback) { // 如果任务执行完返回了一个函数,说明任务没干完被中断了
currentTask.callback = continuationCallback;
advanceTimers(currentTime);
hasMoreWork = !0;
break b; // 跳出循环,准备让出控制权
}
currentTask === peek(taskQueue) && pop(taskQueue); // 出队列
advanceTimers(currentTime); // 再次排序
} else pop(taskQueue); // 当前任务callback不是函数,直接出队列
currentTask = peek(taskQueue); // 再次取出堆顶
}
if (null !== currentTask) hasMoreWork = !0;
else {
var firstTimer = peek(timerQueue);
null !== firstTimer &&
requestHostTimeout(
handleTimeout,
firstTimer.startTime - currentTime
);
hasMoreWork = !1;
}
}
break a;
} finally {
(currentTask = null),
(currentPriorityLevel = previousPriorityLevel),
(isPerformingWork = !1); // 结束工作,重置状态
}
hasMoreWork = void 0;
}
} finally {
// 如果活没干完,再发一个宏任务消息,重新排队
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = !1;
}
}
}
}
// ---- 小根堆相关api ------
function push(heap, node) {
var index = heap.length;
heap.push(node);
a: for (; 0 < index; ) {
var parentIndex = (index - 1) >>> 1,
parent = heap[parentIndex];
if (0 < compare(parent, node))
(heap[parentIndex] = node),
(heap[index] = parent),
(index = parentIndex);
else break a;
}
}
function peek(heap) {
return 0 === heap.length ? null : heap[0];
}
function pop(heap) {
if (0 === heap.length) return null;
var first = heap[0],
last = heap.pop();
if (last !== first) {
heap[0] = last;
a: for (
var index = 0, length = heap.length, halfLength = length >>> 1;
index < halfLength;
) {
var leftIndex = 2 * (index + 1) - 1,
left = heap[leftIndex],
rightIndex = leftIndex + 1,
right = heap[rightIndex];
if (0 > compare(left, last))
rightIndex < length && 0 > compare(right, left)
? ((heap[index] = right),
(heap[rightIndex] = last),
(index = rightIndex))
: ((heap[index] = left),
(heap[leftIndex] = last),
(index = leftIndex));
else if (rightIndex < length && 0 > compare(right, last))
(heap[index] = right),
(heap[rightIndex] = last),
(index = rightIndex);
else break a;
}
}
return first;
}
function compare(a, b) {
var diff = a.sortIndex - b.sortIndex;
return 0 !== diff ? diff : a.id - b.id;
}
// 把 timerQueue 里到期的任务挪到 taskQueue 里
function advanceTimers(currentTime) {
for (var timer = peek(timerQueue); null !== timer; ) {
if (null === timer.callback) pop(timerQueue);
else if (timer.startTime <= currentTime)
pop(timerQueue),
(timer.sortIndex = timer.expirationTime),
push(taskQueue, timer);
else break;
timer = peek(timerQueue);
}
}
function handleTimeout(currentTime) {
isHostTimeoutScheduled = !1;
advanceTimers(currentTime);
if (!isHostCallbackScheduled)
if (null !== peek(taskQueue))
(isHostCallbackScheduled = !0),
isMessageLoopRunning ||
((isMessageLoopRunning = !0), schedulePerformWorkUntilDeadline());
else {
var firstTimer = peek(timerQueue);
null !== firstTimer &&
requestHostTimeout(
handleTimeout,
firstTimer.startTime - currentTime
);
}
}
function shouldYieldToHost() {
return needsPaint
? !0 // 翻译:如果 needsPaint 是 true,立刻返回 true (让出主线程)
: exports.unstable_now() - startTime < frameInterval
? !1 // 翻译:如果没有到 5ms,返回 false (继续干活)
: !0; // 翻译:如果超过了 5ms,返回 true (让出主线程)
}
function requestHostTimeout(callback, ms) {
taskTimeoutID = localSetTimeout(function () {
callback(exports.unstable_now());
}, ms);
}
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
"function" ===
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart &&
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(Error());
// performance.now() 的 降级方案
exports.unstable_now = void 0;
if (
"object" === typeof performance &&
"function" === typeof performance.now
) {
var localPerformance = performance;
exports.unstable_now = function () {
return localPerformance.now();
};
} else {
var localDate = Date,
initialTime = localDate.now();
exports.unstable_now = function () {
return localDate.now() - initialTime;
};
}
var taskQueue = [],
timerQueue = [],
taskIdCounter = 1,
currentTask = null,
currentPriorityLevel = 3,
isPerformingWork = !1,
isHostCallbackScheduled = !1,
isHostTimeoutScheduled = !1,
needsPaint = !1,
localSetTimeout = "function" === typeof setTimeout ? setTimeout : null,
localClearTimeout =
"function" === typeof clearTimeout ? clearTimeout : null,
localSetImmediate =
"undefined" !== typeof setImmediate ? setImmediate : null,
isMessageLoopRunning = !1, // 核心标志位:表示是否已经安排了宏任务
taskTimeoutID = -1,
frameInterval = 5,
startTime = -1;
// schedulePerformWorkUntilDeadline降级
// 1. 首选:setImmediate (主要针对 Node.js 和 老版 IE)
if ("function" === typeof localSetImmediate)
var schedulePerformWorkUntilDeadline = function () {
localSetImmediate(performWorkUntilDeadline);
};
// 2. 备选:MessageChannel (绝大部分现代浏览器,如 Chrome, Safari, Firefox)
// MessageChannel 是 HTML5 引入的一个 API,允许我们创建一个新的消息通道,包含两个 MessagePort 对象。
// 我们可以在任意一个 port 上发送消息,而另一个 port 则会触发 onmessage 事件。
// 这使得 MessageChannel 成为一种非常高效的方式来安排任务在浏览器的事件循环中执行。
else if ("undefined" !== typeof MessageChannel) {
var channel = new MessageChannel(),
port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = function () {
port.postMessage(null); // 发送消息,触发宏任务
};
} else // 3. 兜底方案:setTimeout
schedulePerformWorkUntilDeadline = function () {
localSetTimeout(performWorkUntilDeadline, 0);
};
// 调度优先级
exports.unstable_IdlePriority = 5;
exports.unstable_ImmediatePriority = 1;
exports.unstable_LowPriority = 4;
exports.unstable_NormalPriority = 3;
exports.unstable_Profiling = null;
exports.unstable_UserBlockingPriority = 2;
exports.unstable_cancelCallback = function (task) {
task.callback = null;
};
// 保持fps稳定在0-125之间,过高的fps不受支持
exports.unstable_forceFrameRate = function (fps) {
0 > fps || 125 < fps
? console.error(
"forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"
)
: (frameInterval = 0 < fps ? Math.floor(1e3 / fps) : 5);
};
exports.unstable_getCurrentPriorityLevel = function () {
return currentPriorityLevel;
};
exports.unstable_next = function (eventHandler) {
switch (currentPriorityLevel) {
case 1:
case 2:
case 3:
var priorityLevel = 3;
break;
default:
priorityLevel = currentPriorityLevel;
}
var previousPriorityLevel = currentPriorityLevel;
currentPriorityLevel = priorityLevel;
try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
}
};
// 有时候 5ms 的时间切片还没用完,但用户突然触发了某些高频或紧急交互(如点击、输入、滚动等),这时我们就需要立刻让出主线程,去处理这些交互事件,以保证界面响应的流畅性。
exports.unstable_requestPaint = function () {
needsPaint = !0;
};
exports.unstable_runWithPriority = function (priorityLevel, eventHandler) {
switch (priorityLevel) {
case 1:
case 2:
case 3:
case 4:
case 5:
break;
default:
priorityLevel = 3;
}
var previousPriorityLevel = currentPriorityLevel;
currentPriorityLevel = priorityLevel;
try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
}
};
exports.unstable_scheduleCallback = function (
priorityLevel,
callback,
options
) {
// 1. 计算任务的“开始时间” (startTime)
// 判断开发者有没有传入 delay(延迟执行时间)。
// 如果有 delay,任务的开始时间就是 当前时间 + delay。
// 如果没有,开始时间就是 当前时间。
var currentTime = exports.unstable_now(); // 相当于performance.now(),获取时间戳,用来代码执行时间分片
if ("object" === typeof options && null !== options) {
options = options.delay; // 变量复用 options 从配置对象变成了 delay 数值
if (typeof options === "number" && options > 0) {
options = currentTime + options; // options 变成了 startTime
} else {
options = currentTime;
}
} else {
options = currentTime;
}
// 2. 计算任务的“过期时间” (expirationTime)
// 根据权重来计算时间片,默认为5000ms
// Scheduler 内部定义了 5 种优先级。优先级越高,timeout 越小,意味着任务越容易“过期”。
// 一旦任务过期,即使破坏帧率,React 也会强制同步执行它。
switch (priorityLevel) {
case 1: var timeout = -1; break; // Immediate (立即执行,已经过期了)
case 2: timeout = 250; break; // UserBlocking (用户阻塞级,如点击事件)
case 5: timeout = 1073741823; break; // Idle (空闲,相当于永不过期)
case 4: timeout = 1e4; break; // Low (低优先级)
default: timeout = 5e3; // Normal (普通优先级,默认 5000ms)
}
timeout = options + timeout; // timeout 变成了 expirationTime (开始时间 + 超时时间)
// 3. 组装“任务对象” (Task Object)
priorityLevel = { // 变量复用
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: options, // options真实语义:startTime
expirationTime: timeout, // timeout真实语义:expirationTime
sortIndex: -1
};
// 4. 双队列分发与唤醒
// 两个小根堆:timerQueue:未到时间的任务(候补区)。taskQueue:已到时间的就绪任务(核心工作区)。
// 逻辑like:如果你预约的是明天的号,就先去 timerQueue 呆着,系统会定个闹钟;如果你挂的是今天的号,就直接进 taskQueue,并立刻呼叫医生(唤醒 workLoop)开始接诊。
if (options > currentTime) {
// // 场景 A:startTime > currentTime,说明这是一个【延迟任务】
priorityLevel.sortIndex = options; // 延迟任务按照 startTime 排序
push(timerQueue, priorityLevel); // 放进专门存放延迟任务的 timerQueue
// 如果当前没有就绪任务,且这是当前任务在堆顶,也就是最先需要唤醒的延迟任务
if (peek(taskQueue) === null && priorityLevel === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
localClearTimeout(taskTimeoutID);
taskTimeoutID = -1;
} else {
isHostTimeoutScheduled = true;
}
// 设置一个定时器 (setTimeout),到点后把它移到 taskQueue 里
requestHostTimeout(handleTimeout, options - currentTime);
}
} else {
// 场景 B:这是一个【立即需要执行的任务】
priorityLevel.sortIndex = timeout; // 就绪任务按照 expirationTime 排序
push(taskQueue, priorityLevel); // 放进就绪队列 taskQueue
// 如果现在没有在执行任务,就通知宿主环境(浏览器)启动宏任务开始干活!
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
}
return priorityLevel;
};
exports.unstable_shouldYield = shouldYieldToHost;
exports.unstable_wrapCallback = function (callback) {
var parentPriorityLevel = currentPriorityLevel;
return function () {
var previousPriorityLevel = currentPriorityLevel;
currentPriorityLevel = parentPriorityLevel;
try {
return callback.apply(this, arguments);
} finally {
currentPriorityLevel = previousPriorityLevel;
}
};
};
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
"function" ===
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop(Error());
})();