Appearance
第5章 调度系统
本章将深入React的调度系统,理解Scheduler包的实现原理、任务优先级模型、最小堆数据结构和时间切片机制。这是理解React并发特性的关键。
在上一章中,我们学习了Fiber架构,理解了React如何将渲染工作分解成可中断的小任务。但是,有了可中断的任务还不够,我们还需要一个调度器来决定:哪些任务应该先执行?什么时候应该让出控制权?如何在多个任务之间切换?
这就是Scheduler包的职责。Scheduler是React的任务调度器,它独立于React核心,可以单独使用。它实现了基于优先级的任务调度、时间切片和任务中断恢复等核心功能。
为什么React需要自己的调度器?浏览器的requestIdleCallback不够用吗?Scheduler如何管理任务队列?Lane优先级模型是如何工作的?时间切片是如何实现的?
本章将逐一解答这些问题,带你深入理解React调度系统的设计与实现。
5.1 Scheduler包概述
Scheduler是React团队开发的独立调度器包,它为React提供了任务调度能力,但设计上与React解耦,可以在其他项目中使用。
5.1.1 独立的调度器
Scheduler包位于packages/scheduler目录,它是一个独立的npm包,可以单独安装使用:
bash
npm install schedulerScheduler的核心功能包括:
- 优先级调度:支持5种优先级级别,高优先级任务优先执行
- 时间切片:将长任务分解成多个时间片,避免阻塞主线程
- 任务中断与恢复:任务可以被中断,稍后继续执行
- 延迟任务:支持延迟执行的任务
Scheduler的基本使用
javascript
import {
unstable_scheduleCallback as scheduleCallback,
unstable_NormalPriority as NormalPriority,
unstable_UserBlockingPriority as UserBlockingPriority,
} from 'scheduler';
// 调度一个普通优先级任务
scheduleCallback(NormalPriority, () => {
console.log('Normal priority task');
});
// 调度一个用户阻塞优先级任务(更高优先级)
scheduleCallback(UserBlockingPriority, () => {
console.log('User blocking task');
// 这个任务会先执行
});为什么不使用浏览器的requestIdleCallback?
浏览器提供了requestIdleCallback API,可以在浏览器空闲时执行任务。但React没有使用它,原因包括:
- 兼容性问题:Safari不支持
requestIdleCallback - 执行频率低:
requestIdleCallback只在浏览器完全空闲时才执行,可能导致任务延迟 - 无法控制优先级:
requestIdleCallback不支持任务优先级 - 无法精确控制时间片:React需要更精细的时间控制
因此,React实现了自己的调度器,使用MessageChannel(或setTimeout)来实现时间切片。
5.1.2 与React的解耦设计
Scheduler被设计为独立的包,与React核心解耦。这种设计有几个优势:
1. 职责分离
┌─────────────────────────────────────────────────────────────┐
│ React应用 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ react │
│ (组件API、Hooks、createElement) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ react-reconciler │
│ (Fiber协调、更新调度、Hooks实现) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ scheduler │
│ (任务调度、优先级管理、时间切片) │
└─────────────────────────────────────────────────────────────┘- react:定义组件API
- react-reconciler:实现协调算法,决定需要做什么工作
- scheduler:决定何时做这些工作
2. 可复用性
Scheduler可以在其他需要任务调度的场景中使用,不仅限于React。例如:
javascript
// 在非React项目中使用Scheduler
import { scheduleCallback, NormalPriority } from 'scheduler';
function processLargeDataset(data) {
let index = 0;
function processChunk() {
const startTime = performance.now();
// 处理一批数据
while (index < data.length && performance.now() - startTime < 5) {
processItem(data[index]);
index++;
}
// 如果还有数据,继续调度
if (index < data.length) {
return processChunk; // 返回continuation
}
}
scheduleCallback(NormalPriority, processChunk);
}3. 测试和维护
独立的包更容易测试和维护。Scheduler有自己的测试套件,可以独立于React进行测试。
Scheduler的核心文件
Scheduler包的主要文件结构:
packages/scheduler/
├── src/
│ ├── Scheduler.js # 主入口(根据平台选择fork)
│ ├── SchedulerMinHeap.js # 最小堆实现
│ ├── SchedulerPriorities.js # 优先级常量定义
│ ├── SchedulerFeatureFlags.js # 特性开关
│ └── forks/
│ ├── Scheduler.js # 浏览器/Node环境实现
│ └── SchedulerMock.js # 测试环境实现
└── index.js核心实现在forks/Scheduler.js中,这是我们重点分析的文件。
5.2 任务优先级
Scheduler支持5种优先级级别,不同优先级的任务有不同的超时时间和执行顺序。
5.2.1 五种优先级级别
Scheduler定义了5种优先级,从高到低依次为:
javascript
// 文件:packages/scheduler/src/SchedulerPriorities.js
export const NoPriority = 0;
export const ImmediatePriority = 1; // 立即执行
export const UserBlockingPriority = 2; // 用户阻塞
export const NormalPriority = 3; // 正常优先级
export const LowPriority = 4; // 低优先级
export const IdlePriority = 5; // 空闲优先级每种优先级对应不同的超时时间:
javascript
// 文件:packages/scheduler/src/forks/Scheduler.js
// 行号:340-365(React 19.3.0)
var timeout;
switch (priorityLevel) {
case ImmediatePriority:
// 立即超时(-1ms)
timeout = -1;
break;
case UserBlockingPriority:
// 250ms后超时
timeout = userBlockingPriorityTimeout; // 250
break;
case IdlePriority:
// 永不超时
timeout = maxSigned31BitInt; // 1073741823
break;
case LowPriority:
// 10秒后超时
timeout = lowPriorityTimeout; // 10000
break;
case NormalPriority:
default:
// 5秒后超时
timeout = normalPriorityTimeout; // 5000
break;
}
var expirationTime = startTime + timeout;优先级与超时时间的关系
| 优先级 | 超时时间 | 典型场景 |
|---|---|---|
| ImmediatePriority | -1ms(立即) | 同步任务、紧急更新 |
| UserBlockingPriority | 250ms | 用户交互(点击、输入) |
| NormalPriority | 5000ms | 数据获取、普通更新 |
| LowPriority | 10000ms | 分析、日志 |
| IdlePriority | 永不超时 | 预加载、缓存 |
超时时间的作用是:如果一个任务在超时时间内没有被执行,它会被标记为过期,必须立即执行,即使会阻塞主线程。
5.2.2 优先级与用户体验
不同优先级的任务对应不同的用户体验需求:
ImmediatePriority(立即优先级)
用于必须同步执行的任务,例如:
- 受控输入框的更新
- 同步的
flushSync调用
jsx
import { flushSync } from 'react-dom';
function SearchBox() {
const [query, setQuery] = useState('');
const handleChange = (e) => {
// 立即同步更新,确保输入框响应
flushSync(() => {
setQuery(e.target.value);
});
};
return <input value={query} onChange={handleChange} />;
}UserBlockingPriority(用户阻塞优先级)
用于用户交互产生的更新,例如:
- 点击按钮
- 悬停效果
- 拖拽操作
jsx
function Counter() {
const [count, setCount] = useState(0);
// 点击事件产生的更新是UserBlockingPriority
const handleClick = () => {
setCount(c => c + 1);
};
return <button onClick={handleClick}>{count}</button>;
}NormalPriority(正常优先级)
用于普通的更新,例如:
- 数据获取完成后的更新
- 定时器触发的更新
- 网络请求的响应
jsx
function DataDisplay() {
const [data, setData] = useState(null);
useEffect(() => {
// 数据获取完成后的更新是NormalPriority
fetchData().then(result => {
setData(result);
});
}, []);
return <div>{data}</div>;
}LowPriority(低优先级)
用于不紧急的更新,例如:
- 分析数据收集
- 日志记录
- 预加载资源
IdlePriority(空闲优先级)
用于可以无限延迟的任务,例如:
- 离屏内容的预渲染
- 缓存预热
- 后台数据同步
jsx
function App() {
useEffect(() => {
// 在空闲时预加载下一页的数据
scheduleCallback(IdlePriority, () => {
prefetchNextPage();
});
}, []);
return <div>...</div>;
}优先级的动态调整
在React中,优先级不是固定的,可以根据情况动态调整。例如:
javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
function ensureRootIsScheduled(root, currentTime) {
const nextLanes = getNextLanes(root, NoLanes);
// 如果有过期的lane,使用同步优先级
if (includesExpiredLane(root, nextLanes)) {
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 否则根据lane计算优先级
const schedulerPriorityLevel = lanesToSchedulerPriority(nextLanes);
scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
}当一个任务等待时间过长,超过了它的超时时间,React会将其提升为同步优先级,确保它能够立即执行。
优先级与饥饿问题
如果高优先级任务不断产生,低优先级任务可能永远得不到执行,这就是"饥饿"问题。Scheduler通过超时机制解决这个问题:
时间线:
0ms ─── 低优先级任务A加入队列(超时时间:10000ms)
100ms ─── 高优先级任务B加入队列
100ms ─── 执行任务B
200ms ─── 高优先级任务C加入队列
200ms ─── 执行任务C
...
10000ms ─── 任务A超时,必须执行
10000ms ─── 执行任务A(即使有新的高优先级任务)这确保了即使是低优先级任务,也能在合理的时间内得到执行。
5.3 最小堆实现
Scheduler使用最小堆(Min Heap)数据结构来管理任务队列。理解最小堆的实现是理解Scheduler工作原理的关键。
5.3.1 为什么使用最小堆
Scheduler需要频繁地执行以下操作:
- 插入任务:将新任务加入队列
- 获取最高优先级任务:找到最紧急的任务
- 移除已执行的任务:从队列中删除任务
不同数据结构的性能对比:
| 数据结构 | 插入 | 获取最小值 | 删除最小值 |
|---|---|---|---|
| 无序数组 | O(1) | O(n) | O(n) |
| 有序数组 | O(n) | O(1) | O(1) |
| 最小堆 | O(log n) | O(1) | O(log n) |
最小堆在插入和删除操作上都有良好的性能,且获取最小值是O(1),非常适合任务调度场景。
什么是最小堆?
最小堆是一种完全二叉树,满足以下性质:
- 堆序性:每个节点的值都小于或等于其子节点的值
- 完全二叉树:除了最后一层,其他层都是满的,最后一层从左到右填充
最小堆示例:
1
/ \
3 2
/ \ / \
7 5 4 6
数组表示:[1, 3, 2, 7, 5, 4, 6]
索引: 0 1 2 3 4 5 6
父子节点关系:
- 节点i的父节点:(i - 1) >>> 1
- 节点i的左子节点:(i + 1) * 2 - 1
- 节点i的右子节点:(i + 1) * 25.3.2 push与pop操作
Scheduler的最小堆实现在SchedulerMinHeap.js文件中。
push操作(插入节点)
javascript
// 文件:packages/scheduler/src/SchedulerMinHeap.js
// 行号:16-21(React 19.3.0)
export function push<T: Node>(heap: Heap<T>, node: T): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}插入步骤:
- 将新节点添加到数组末尾
- 执行"上浮"操作,维护堆序性
siftUp(上浮)操作
javascript
// 文件:packages/scheduler/src/SchedulerMinHeap.js
// 行号:45-60(React 19.3.0)
function siftUp<T: Node>(heap: Heap<T>, node: T, i: number): void {
let index = i;
while (index > 0) {
const parentIndex = (index - 1) >>> 1;
const parent = heap[parentIndex];
if (compare(parent, node) > 0) {
// 父节点更大,交换位置
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// 父节点更小,停止
return;
}
}
}上浮过程示例:
插入节点1到堆中:
初始堆:
3
/ \
5 4
/
7
步骤1:添加到末尾
3
/ \
5 4
/ \
7 1
步骤2:与父节点5比较,1 < 5,交换
3
/ \
1 4
/ \
7 5
步骤3:与父节点3比较,1 < 3,交换
1
/ \
3 4
/ \
7 5
完成!pop操作(移除最小节点)
javascript
// 文件:packages/scheduler/src/SchedulerMinHeap.js
// 行号:27-39(React 19.3.0)
export function pop<T: Node>(heap: Heap<T>): T | null {
if (heap.length === 0) {
return null;
}
const first = heap[0];
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
}移除步骤:
- 保存根节点(最小值)
- 将最后一个节点移到根位置
- 执行"下沉"操作,维护堆序性
siftDown(下沉)操作
javascript
// 文件:packages/scheduler/src/SchedulerMinHeap.js
// 行号:62-90(React 19.3.0)
function siftDown<T: Node>(heap: Heap<T>, node: T, i: number): void {
let index = i;
const length = heap.length;
const halfLength = length >>> 1;
while (index < halfLength) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// 找到左右子节点中较小的一个
if (compare(left, node) < 0) {
if (rightIndex < length && compare(right, left) < 0) {
// 右子节点最小
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// 左子节点最小
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (rightIndex < length && compare(right, node) < 0) {
// 只有右子节点更小
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// 当前节点已经是最小的,停止
return;
}
}
}下沉过程示例:
从堆中移除最小节点:
初始堆:
1
/ \
3 2
/ \
7 5
步骤1:移除根节点1,将最后一个节点5移到根
5
/ \
3 2
/
7
步骤2:与子节点比较,2最小,交换
2
/ \
3 5
/
7
完成!peek操作(查看最小节点)
javascript
// 文件:packages/scheduler/src/SchedulerMinHeap.js
// 行号:23-25(React 19.3.0)
export function peek<T: Node>(heap: Heap<T>): T | null {
return heap.length === 0 ? null : heap[0];
}peek操作非常简单,直接返回数组的第一个元素,时间复杂度O(1)。
compare函数(比较节点)
javascript
// 文件:packages/scheduler/src/SchedulerMinHeap.js
// 行号:92-96(React 19.3.0)
function compare(a: Node, b: Node) {
// 首先比较sortIndex,然后比较id
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}比较规则:
- 优先比较
sortIndex(通常是过期时间) - 如果
sortIndex相同,比较id(保证FIFO顺序)
5.3.3 示例:手写SchedulerMinHeap
为了更好地理解最小堆的工作原理,让我们手写一个简化版的SchedulerMinHeap:
javascript
// 简化版最小堆实现
class MinHeap {
constructor() {
this.heap = [];
}
// 插入节点
push(node) {
this.heap.push(node);
this.siftUp(this.heap.length - 1);
}
// 移除最小节点
pop() {
if (this.heap.length === 0) return null;
const min = this.heap[0];
const last = this.heap.pop();
if (this.heap.length > 0) {
this.heap[0] = last;
this.siftDown(0);
}
return min;
}
// 查看最小节点
peek() {
return this.heap.length > 0 ? this.heap[0] : null;
}
// 上浮操作
siftUp(index) {
const node = this.heap[index];
while (index > 0) {
const parentIndex = Math.floor((index - 1) / 2);
const parent = this.heap[parentIndex];
if (node.priority < parent.priority) {
this.heap[index] = parent;
index = parentIndex;
} else {
break;
}
}
this.heap[index] = node;
}
// 下沉操作
siftDown(index) {
const node = this.heap[index];
const length = this.heap.length;
const halfLength = Math.floor(length / 2);
while (index < halfLength) {
const leftIndex = 2 * index + 1;
const rightIndex = leftIndex + 1;
let minIndex = leftIndex;
if (rightIndex < length &&
this.heap[rightIndex].priority < this.heap[leftIndex].priority) {
minIndex = rightIndex;
}
if (this.heap[minIndex].priority < node.priority) {
this.heap[index] = this.heap[minIndex];
index = minIndex;
} else {
break;
}
}
this.heap[index] = node;
}
}
// 使用示例
const heap = new MinHeap();
heap.push({ id: 1, priority: 5, task: 'Task A' });
heap.push({ id: 2, priority: 1, task: 'Task B' });
heap.push({ id: 3, priority: 3, task: 'Task C' });
console.log(heap.peek()); // { id: 2, priority: 1, task: 'Task B' }
console.log(heap.pop()); // { id: 2, priority: 1, task: 'Task B' }
console.log(heap.peek()); // { id: 3, priority: 3, task: 'Task C' }这个简化版本展示了最小堆的核心逻辑。React的实现更加优化,使用了位运算来计算父子节点索引,性能更好。
5.4 Lane优先级模型
React 18引入了Lane优先级模型,替代了之前的ExpirationTime模型。Lane使用位运算来表示优先级,更加灵活和高效。
5.4.1 Lane的位运算设计
Lane是一个32位整数,每一位代表一个优先级"车道"(Lane)。多个Lane可以通过位运算合并或分离。
javascript
// 文件:packages/react-reconciler/src/ReactFiberLane.js
// 行号:40-120(React 19.3.0)
export type Lanes = number;
export type Lane = number;
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const GestureLane: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111100000000;
const TransitionLane1: Lane = /* */ 0b0000000000000000000000100000000;
const TransitionLane2: Lane = /* */ 0b0000000000000000000001000000000;
// ... TransitionLane3-14
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
export const IdleHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;为什么使用位运算?
- 高效的合并操作:使用按位或(|)合并多个Lane
- 高效的检查操作:使用按位与(&)检查是否包含某个Lane
- 高效的移除操��:使用按位与非(&~)移除某个Lane
- 节省内存:一个32位整数可以表示31个优先级
javascript
// Lane的位运算操作
// 合并Lane
const lanes = SyncLane | DefaultLane;
// 0b0000000000000000000000000000010 | 0b0000000000000000000000000100000
// = 0b0000000000000000000000000100010
// 检查是否包含某个Lane
if (lanes & SyncLane) {
// 包含SyncLane
}
// 移除某个Lane
const newLanes = lanes & ~SyncLane;
// 0b0000000000000000000000000100010 & ~0b0000000000000000000000000000010
// = 0b0000000000000000000000000100000
// 检查是否为空
if (lanes === NoLanes) {
// 没有任何Lane
}5.4.2 常见Lane类型
React定义了多种Lane类型,每种对应不同的更新场景:
同步Lane(SyncLane)
最高优先级,用于同步更新:
jsx
import { flushSync } from 'react-dom';
function App() {
const [count, setCount] = useState(0);
const handleClick = () => {
// 使用flushSync强制同步更新
flushSync(() => {
setCount(c => c + 1);
});
// 此时DOM已经更新
console.log('Count updated');
};
return <button onClick={handleClick}>{count}</button>;
}输入连续Lane(InputContinuousLane)
用于连续的用户输入,如拖拽、滚动:
jsx
function DraggableBox() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleDrag = (e) => {
// 拖拽事件产生InputContinuousLane更新
setPosition({ x: e.clientX, y: e.clientY });
};
return <div onDrag={handleDrag} style={{
transform: `translate(${position.x}px, ${position.y}px)`
}} />;
}默认Lane(DefaultLane)
用于普通的用户交互,如点击、输入:
jsx
function Counter() {
const [count, setCount] = useState(0);
// 点击事件产生DefaultLane更新
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}过渡Lane(TransitionLanes)
用于非紧急的更新,可以被中断:
jsx
import { useTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
const handleSearch = (value) => {
setQuery(value); // 立即更新输入框(DefaultLane)
startTransition(() => {
// 搜索结果更新使用TransitionLane,可以被中断
const filtered = expensiveSearch(value);
setResults(filtered);
});
};
return (
<div>
<input value={query} onChange={e => handleSearch(e.target.value)} />
{isPending && <div>搜索中...</div>}
<ResultList results={results} />
</div>
);
}空闲Lane(IdleLane)
用于低优先级的后台任务:
jsx
function App() {
useEffect(() => {
// 在空闲时预加载数据
requestIdleCallback(() => {
prefetchData();
});
}, []);
return <div>...</div>;
}5.4.3 Lane的合并与分离
Lane模型的强大之处在于可以灵活地合并和分离优先级。
合并Lane
当多个更新同时发生时,React会合并它们的Lane:
javascript
// 文件:packages/react-reconciler/src/ReactFiberLane.js
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
// 示例
const lane1 = SyncLane; // 0b0000000000000000000000000000010
const lane2 = DefaultLane; // 0b0000000000000000000000000100000
const merged = mergeLanes(lane1, lane2);
// merged = 0b0000000000000000000000000100010检查Lane
检查一个Lanes集合是否包含特定的Lane:
javascript
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane): boolean {
return (a & b) !== NoLanes;
}
// 示例
const lanes = SyncLane | DefaultLane;
if (includesSomeLane(lanes, SyncLane)) {
// lanes包含SyncLane
}移除Lane
从Lanes集合中移除特定的Lane:
javascript
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}
// 示例
const lanes = SyncLane | DefaultLane | IdleLane;
const newLanes = removeLanes(lanes, DefaultLane);
// newLanes = SyncLane | IdleLane获取最高优先级Lane
从Lanes集合中获取优先级最高的Lane:
javascript
export function getHighestPriorityLane(lanes: Lanes): Lane {
return lanes & -lanes;
}
// 示例
const lanes = DefaultLane | IdleLane;
// 0b0010000000000000000000000100000
const highest = getHighestPriorityLane(lanes);
// highest = DefaultLane (0b0000000000000000000000000100000)这个技巧利用了补码的性质:lanes & -lanes会保留最右边的1,即最高优先级的Lane。
Lane与Scheduler优先级的转换
React需要将Lane转换为Scheduler的优先级:
javascript
// 文件:packages/react-reconciler/src/ReactFiberLane.js
export function lanesToSchedulerPriority(lanes: Lanes): PriorityLevel {
const lane = getHighestPriorityLane(lanes);
if ((lane & SyncUpdateLanes) !== NoLanes) {
return ImmediatePriority;
}
if ((lane & InputContinuousLane) !== NoLanes) {
return UserBlockingPriority;
}
if ((lane & DefaultLane) !== NoLanes) {
return NormalPriority;
}
if ((lane & TransitionLanes) !== NoLanes) {
return NormalPriority;
}
if ((lane & RetryLanes) !== NoLanes) {
return NormalPriority;
}
if ((lane & IdleLane) !== NoLanes) {
return IdlePriority;
}
return NormalPriority;
}Lane的过期机制
Lane也有过期时间,当一个Lane等待时间过长时,会被标记为过期:
javascript
// 文件:packages/react-reconciler/src/ReactFiberLane.js
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
const pendingLanes = root.pendingLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
// 首次调度,设置过期时间
expirationTimes[index] = computeExpirationTime(lane, currentTime);
} else if (expirationTime <= currentTime) {
// 已过期,标记为过期Lane
root.expiredLanes |= lane;
}
lanes &= ~lane;
}
}过期的Lane会被提升为同步优先级,确保它们能够立即执行。
5.5 时间切片
时间切片(Time Slicing)是Scheduler的核心功能之一,它允许React将长任务分解成多个小任务,在每个时间片之间让出控制权给浏览器。
5.5.1 shouldYield判断
Scheduler使用shouldYieldToHost函数来判断是否应该让出控制权:
javascript
// 文件:packages/scheduler/src/forks/Scheduler.js
// 行号:550-570(React 19.3.0)
let frameInterval = frameYieldMs; // 默认5ms
let startTime = -1;
function shouldYieldToHost(): boolean {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < frameInterval) {
// 时间片还没用完
return false;
}
// 时间片用完了,需要让出控制权
if (enableRequestPaint) {
// 如果需要绘制,也让出控制权
if (needsPaint) {
return true;
}
}
return true;
}时间片的大小
默认的时间片大小是5ms。这个值是经过权衡的:
- 太小(如1ms):频繁让出控制权,调度开销大
- 太大(如50ms):可能导致页面卡顿
5ms是一个平衡点,既能保证流畅性,又不会有太大的调度开销。
javascript
// 文件:packages/scheduler/src/SchedulerFeatureFlags.js
export const frameYieldMs = 5; // 5毫秒动态调整时间片
在某些情况下,React会动态调整时间片大小。例如,如果检测到需要绘制(paint),会立即让出控制权:
javascript
function requestPaint() {
if (enableRequestPaint) {
needsPaint = true;
}
}5.5.2 MessageChannel的使用
Scheduler使用MessageChannel来实现时间切片。为什么不用setTimeout?
setTimeout的问题
- 最小延迟:
setTimeout(fn, 0)实际延迟约4ms(浏览器限制) - 嵌套限制:嵌套5层以上的
setTimeout会被限制为4ms - 优先级低:
setTimeout的优先级低于其他宏任务
MessageChannel的优势
- 无最小延迟:可以立即执行
- 无嵌套限制:不受嵌套层数影响
- 优先级适中:在宏任务队列中,优先级适中
javascript
// 文件:packages/scheduler/src/forks/Scheduler.js
// 行号:600-650(React 19.3.0)
let schedulePerformWorkUntilDeadline;
if (typeof MessageChannel !== 'undefined') {
// 使用MessageChannel
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// 降级到setTimeout
schedulePerformWorkUntilDeadline = () => {
setTimeout(performWorkUntilDeadline, 0);
};
}
function performWorkUntilDeadline() {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true;
let hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(currentTime);
} finally {
if (hasMoreWork) {
// 还有工作,继续调度
schedulePerformWorkUntilDeadline();
} else {
scheduledHostCallback = null;
}
}
}
}工作流程
1. React调度一个任务
↓
2. schedulePerformWorkUntilDeadline()
↓
3. port.postMessage(null)
↓
4. 浏览器事件循环
↓
5. performWorkUntilDeadline()
↓
6. 执行任务(最多5ms)
↓
7. shouldYieldToHost() 检查
↓
8. 如果还有工作,回到步骤25.5.3 示例:时间切片效果演示
让我们通过一个示例来演示时间切片的效果:
jsx
import { useState, useTransition } from 'react';
function HeavyComponent({ items }) {
// 模拟耗时的渲染
const startTime = performance.now();
while (performance.now() - startTime < 3) {
// 阻塞3ms
}
return (
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
function App() {
const [items, setItems] = useState([]);
const [isPending, startTransition] = useTransition();
const handleGenerate = () => {
// 生成1000个项目
const newItems = Array.from({ length: 1000 }, (_, i) => ({
id: i,
name: `Item ${i}`
}));
// 使用transition,启用时间切片
startTransition(() => {
setItems(newItems);
});
};
return (
<div>
<button onClick={handleGenerate}>生成列表</button>
<input placeholder="测试输入响应性" />
{isPending && <div>加载中...</div>}
<HeavyComponent items={items} />
</div>
);
}没有时间切片的情况
时间线:
0ms ─── 点击按钮
0ms ─── 开始渲染1000个组件
3000ms ─── 渲染完成
─── 期间输入框无响应,页面卡顿有时间切片的情况
时间线:
0ms ─── 点击按钮
0ms ─── 开始渲染(时间片1)
5ms ─── 让出控制权
5ms ─── 用户输入,立即响应
10ms ─── 继续渲染(时间片2)
15ms ─── 让出控制权
...
3000ms ─── 渲染完成
─── 期间输入框保持响应时间切片的可视化
没有时间切片:
[████████████████████████████████████] 渲染(3000ms)
↑ 用户输入被阻塞
有时间切片:
[█] 渲染5ms
[█] 渲染5ms
[█] 渲染5ms
↑ 用户输入立即响应
[█] 渲染5ms
[█] 渲染5ms
...workLoop中的时间切片
javascript
// 文件:packages/react-reconciler/src/ReactFiberWorkLoop.js
// 简化版
function workLoopConcurrent() {
// 并发模式的工作循环
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
function workLoopSync() {
// 同步模式的工作循环(无时间切片)
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}并发模式下,每次循环都会检查shouldYield(),如果时间片用完就暂停。同步模式下则一直执行到完成。
5.6 示例:手写SchedulerMinHeap
为了加深对Scheduler的理解,让我们手写一个完整的任务调度器,包含最小堆、优先级管理和时间切片。
javascript
// 优先级常量
const ImmediatePriority = 1;
const UserBlockingPriority = 2;
const NormalPriority = 3;
const LowPriority = 4;
const IdlePriority = 5;
// 优先级对应的超时时间(毫秒)
const IMMEDIATE_PRIORITY_TIMEOUT = -1;
const USER_BLOCKING_PRIORITY_TIMEOUT = 250;
const NORMAL_PRIORITY_TIMEOUT = 5000;
const LOW_PRIORITY_TIMEOUT = 10000;
const IDLE_PRIORITY_TIMEOUT = 1073741823; // 永不超时
// 时间切片大小
const FRAME_YIELD_MS = 5;
// 最小堆实现
class MinHeap {
constructor() {
this.heap = [];
}
push(node) {
const index = this.heap.length;
this.heap.push(node);
this.siftUp(index);
}
pop() {
if (this.heap.length === 0) return null;
const first = this.heap[0];
const last = this.heap.pop();
if (this.heap.length > 0 && last !== first) {
this.heap[0] = last;
this.siftDown(0);
}
return first;
}
peek() {
return this.heap.length > 0 ? this.heap[0] : null;
}
siftUp(index) {
const node = this.heap[index];
while (index > 0) {
const parentIndex = (index - 1) >>> 1;
const parent = this.heap[parentIndex];
if (this.compare(parent, node) > 0) {
this.heap[index] = parent;
index = parentIndex;
} else {
break;
}
}
this.heap[index] = node;
}
siftDown(index) {
const node = this.heap[index];
const length = this.heap.length;
const halfLength = length >>> 1;
while (index < halfLength) {
const leftIndex = (index + 1) * 2 - 1;
const left = this.heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = this.heap[rightIndex];
if (this.compare(left, node) < 0) {
if (rightIndex < length && this.compare(right, left) < 0) {
this.heap[index] = right;
this.heap[rightIndex] = node;
index = rightIndex;
} else {
this.heap[index] = left;
this.heap[leftIndex] = node;
index = leftIndex;
}
} else if (rightIndex < length && this.compare(right, node) < 0) {
this.heap[index] = right;
this.heap[rightIndex] = node;
index = rightIndex;
} else {
break;
}
}
this.heap[index] = node;
}
compare(a, b) {
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}
}
// 任务调度器
class TaskScheduler {
constructor() {
this.taskQueue = new MinHeap();
this.timerQueue = new MinHeap();
this.taskIdCounter = 1;
this.isPerformingWork = false;
this.isHostCallbackScheduled = false;
this.currentTask = null;
this.startTime = -1;
}
getCurrentTime() {
return performance.now();
}
scheduleCallback(priorityLevel, callback, options) {
const currentTime = this.getCurrentTime();
// 计算开始时间
let startTime;
if (options && typeof options.delay === 'number' && options.delay > 0) {
startTime = currentTime + options.delay;
} else {
startTime = currentTime;
}
// 计算超时时间
let timeout;
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
const expirationTime = startTime + timeout;
// 创建任务
const newTask = {
id: this.taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1
};
if (startTime > currentTime) {
// 延迟任务,加入timerQueue
newTask.sortIndex = startTime;
this.timerQueue.push(newTask);
if (this.taskQueue.peek() === null && newTask === this.timerQueue.peek()) {
// 所有任务都是延迟的,设置定时器
this.requestHostTimeout(() => {
this.handleTimeout();
}, startTime - currentTime);
}
} else {
// 立即任务,加入taskQueue
newTask.sortIndex = expirationTime;
this.taskQueue.push(newTask);
if (!this.isHostCallbackScheduled && !this.isPerformingWork) {
this.isHostCallbackScheduled = true;
this.requestHostCallback();
}
}
return newTask;
}
handleTimeout() {
const currentTime = this.getCurrentTime();
this.advanceTimers(currentTime);
if (!this.isHostCallbackScheduled) {
if (this.taskQueue.peek() !== null) {
this.isHostCallbackScheduled = true;
this.requestHostCallback();
} else {
const firstTimer = this.timerQueue.peek();
if (firstTimer !== null) {
this.requestHostTimeout(() => {
this.handleTimeout();
}, firstTimer.startTime - currentTime);
}
}
}
}
advanceTimers(currentTime) {
// 将到期的延迟任务移到taskQueue
let timer = this.timerQueue.peek();
while (timer !== null) {
if (timer.callback === null) {
this.timerQueue.pop();
} else if (timer.startTime <= currentTime) {
this.timerQueue.pop();
timer.sortIndex = timer.expirationTime;
this.taskQueue.push(timer);
} else {
return;
}
timer = this.timerQueue.peek();
}
}
workLoop(initialTime) {
let currentTime = initialTime;
this.advanceTimers(currentTime);
this.currentTask = this.taskQueue.peek();
while (this.currentTask !== null) {
if (this.currentTask.expirationTime > currentTime && this.shouldYieldToHost()) {
// 任务未过期且时间片用完,暂停
break;
}
const callback = this.currentTask.callback;
if (typeof callback === 'function') {
this.currentTask.callback = null;
const didUserCallbackTimeout = this.currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = this.getCurrentTime();
if (typeof continuationCallback === 'function') {
// 任务返回了continuation,继续执行
this.currentTask.callback = continuationCallback;
this.advanceTimers(currentTime);
return true;
} else {
if (this.currentTask === this.taskQueue.peek()) {
this.taskQueue.pop();
}
this.advanceTimers(currentTime);
}
} else {
this.taskQueue.pop();
}
this.currentTask = this.taskQueue.peek();
}
// 返回是否还有工作
if (this.currentTask !== null) {
return true;
} else {
const firstTimer = this.timerQueue.peek();
if (firstTimer !== null) {
this.requestHostTimeout(() => {
this.handleTimeout();
}, firstTimer.startTime - currentTime);
}
return false;
}
}
shouldYieldToHost() {
const timeElapsed = this.getCurrentTime() - this.startTime;
return timeElapsed >= FRAME_YIELD_MS;
}
requestHostCallback() {
// 使用MessageChannel实现
if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
channel.port1.onmessage = () => {
this.performWorkUntilDeadline();
};
channel.port2.postMessage(null);
} else {
// 降级到setTimeout
setTimeout(() => {
this.performWorkUntilDeadline();
}, 0);
}
}
requestHostTimeout(callback, ms) {
setTimeout(callback, ms);
}
performWorkUntilDeadline() {
if (this.isHostCallbackScheduled) {
const currentTime = this.getCurrentTime();
this.startTime = currentTime;
this.isPerformingWork = true;
let hasMoreWork = true;
try {
hasMoreWork = this.workLoop(currentTime);
} finally {
this.isPerformingWork = false;
if (hasMoreWork) {
// 还有工作,继续调度
this.requestHostCallback();
} else {
this.isHostCallbackScheduled = false;
}
}
}
}
}
// 使用示例
const scheduler = new TaskScheduler();
// 调度一个普通优先级任务
scheduler.scheduleCallback(NormalPriority, () => {
console.log('Normal priority task executed');
});
// 调度一个高优先级任务
scheduler.scheduleCallback(UserBlockingPriority, () => {
console.log('User blocking task executed');
});
// 调度一个延迟任务
scheduler.scheduleCallback(NormalPriority, () => {
console.log('Delayed task executed');
}, { delay: 1000 });
// 调度一个可中断的长任务
scheduler.scheduleCallback(NormalPriority, (didTimeout) => {
const startTime = performance.now();
let count = 0;
// 模拟耗时工作
while (performance.now() - startTime < 20 && count < 1000000) {
count++;
}
console.log(`Processed ${count} items`);
if (count < 1000000) {
// 还没完成,返回continuation
return (didTimeout) => {
console.log('Continuing work...');
// 继续处理剩余工作
};
}
});这个示例实现了Scheduler的核心功能:
- 最小堆管理任务队列:高效地获取最高优先级任务
- 优先级调度:支持5种优先级级别
- 时间切片:使用
shouldYieldToHost判断是否让出控制权 - 延迟任务:支持延迟执行的任务
- 任务中断与恢复:通过返回continuation实现
本章小结
本章深入探讨了React的调度系统,主要内容包括:
Scheduler包概述:独立的调度器,与React解耦,可复用
任务优先级:5种优先级级别,从ImmediatePriority到IdlePriority,每种优先级有不同的超时时间
最小堆实现:使用最小堆管理任务队列,实现O(log n)的插入和删除,O(1)的查找最小值
Lane优先级模型:使用32位整数表示优先级,通过位运算实现高效的合并、检查和分离操作
时间切片:将长任务分解成多个时间片,每个时间片默认5ms,使用MessageChannel实现调度
调度系统是React并发特性的基础。理解了调度系统,我们就能理解React如何在保证响应性的同时,高效地处理大量更新。
下一章,我们将学习协调过程,看看React如何使用Fiber架构和调度系统来计算UI的变化。
思考题
为什么React要实现自己的调度器,而不使用浏览器的
requestIdleCallback?最小堆相比普通数组有什么优势?在什么场景下最小堆不是最优选择?
Lane模型相比之前的ExpirationTime模型有什么优势?
时间切片的大小(5ms)是如何权衡的?如果改成1ms或50ms会有什么影响?
在你的项目中,哪些场景适合使用
useTransition来启用时间切片?