内部实现:React
Source: Dev.to
介绍
自从我开始使用 React 起,我就想弄清楚它的内部工作原理。这不是对源码的细粒度审查,而是对 React 内部一些关键包和集成的概览。进行这项研究帮助我更好地理解 React 并更有信心地调试问题。希望你也能获得更好的视角。
层次结构
我发现,React 本身作为一个独立的包并不是很有用。它充当 API 层,并大量委托给 react-reconciler 包。这也是它常常与渲染器(如 react-dom)一起使用的原因,后者与 react-reconciler 捆绑在一起。
因此,要了解 React,就必须了解 react-reconciler 以及它的关键依赖——scheduler 包。我们先从 scheduler 开始。
scheduler
scheduler 包通过将工作拆分成小块并管理执行优先级,使 React 的并发特性得以实现。
“对于同步渲染,一旦更新开始渲染,直到用户在屏幕上看到结果之前,任何事情都无法中断它。”
— React v18.0 | 2022 年 3 月 29 日,React 团队
在并发 React 出现之前,更新是同步的,会阻塞主线程直至完成,这导致 UI 卡顿和动画不流畅。
scheduler 通过实现 协作调度 来解决这个问题:任务自愿将控制权让回给宿主,以便主线程可以用于其他工作。任务运行一个短时间间隔(≈ 5 ms),检查是否应该让出,如果截止时间已过则让出,然后等待下一次执行机会。
协作调度由 消息循环 管理,而实际的工作则由 工作循环 处理。
任务、两个循环和两个队列
unstable_scheduleCallback 函数(以下简称 scheduleCallback)是调度并发(非同步)工作的主要方式。
function unstable_scheduleCallback(
priorityLevel: PriorityLevel,
callback: Callback,
options?: { delay: number },
): Task {
// implementation...
}
Task 对象
export opaque type Task = {
id: number,
callback: Callback | null,
priorityLevel: PriorityLevel,
startTime: number,
expirationTime: number,
sortIndex: number,
isQueued?: boolean,
};
优先级
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
scheduler 使用这些级别来管理执行优先级。
队列
scheduler 维护两个优先级队列(使用最小堆实现):
| 队列 | 用途 |
|---|---|
| Task queue | 存放已准备好执行的任务。 |
| Timer queue | 存放尚未满足条件的任务(它们的 startTime 在未来)。 |
创建任务时,会为其分配一个 startTime(任务何时可执行)和一个 expirationTime(任务必须在何时之前完成)。
- 如果
startTime > currentTime,任务进入 Timer queue。 - 否则,直接进入 Task queue。
计算 startTime
// 立即任务
startTime = currentTime; // 现在可用
// 延迟任务
startTime = currentTime + delay; // 将来可用
计算 expirationTime
expirationTime = startTime + timeout;
timeout 取决于 PriorityLevel,从 ImmediatePriority 的 -1 ms 到 LowPriority 大约 10 000 ms 不等。
sortIndex
- 在 Timer queue 中,
sortIndex等于startTime。 - 在 Task queue 中,
sortIndex等于expirationTime。
优先级示例
| 优先级层级 | 常见触发场景 |
|---|---|
ImmediatePriority(同步) | 在现代 React 中很少使用 |
UserBlockingPriority | onScroll、onDrag、onMouseMove |
NormalPriority | startTransition()、useDeferredValue(大多数调度工作默认) |
IdlePriority | 离屏/隐藏内容的渲染 |
同步工作会绕过调度器,使用 queueMicrotask API 加入队列。
工作循环
function workLoop(initialTime: number) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (!enableAlwaysYieldScheduler) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// 任务尚未超时且已到达截止时间。
break;
}
}
// ... execute callback ...
currentTask = peek(taskQueue);
}
}
工作循环从 Task queue 中取出任务并执行其回调,直到必须让给宿主为止。它还会通过 advanceTimers 将 Timer queue 中的任务提升到 Task queue。如果任务运行时间超过了分配的时间片,其 callback 会被替换为 continuation callback,以便调度器稍后继续执行。被取消的任务在出队时会被丢弃。
消息循环
当工作循环让出时,React 需要一种机制来重新启动它。这由 消息循环 负责。React 更倾向使用非标准的 setImmediate API;如果不可用,则回退到 MessageChannel 或 setTimeout(用于非浏览器环境)。
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
const schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
performWorkUntilDeadline 启动工作循环。当工作循环决定让出时,它会调用 schedulePerformWorkUntilDeadline(),该函数发送一条消息,在下一个 tick 触发 performWorkUntilDeadline。使用 MessageChannel 可以避免 setTimeout 强加的 4 ms 最小延迟,从而让 React 更精确地调度工作。