Skip to content

第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 scheduler

Scheduler的核心功能包括:

  1. 优先级调度:支持5种优先级级别,高优先级任务优先执行
  2. 时间切片:将长任务分解成多个时间片,避免阻塞主线程
  3. 任务中断与恢复:任务可以被中断,稍后继续执行
  4. 延迟任务:支持延迟执行的任务

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没有使用它,原因包括:

  1. 兼容性问题:Safari不支持requestIdleCallback
  2. 执行频率低requestIdleCallback只在浏览器完全空闲时才执行,可能导致任务延迟
  3. 无法控制优先级requestIdleCallback不支持任务优先级
  4. 无法精确控制时间片: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(立即)同步任务、紧急更新
UserBlockingPriority250ms用户交互(点击、输入)
NormalPriority5000ms数据获取、普通更新
LowPriority10000ms分析、日志
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需要频繁地执行以下操作:

  1. 插入任务:将新任务加入队列
  2. 获取最高优先级任务:找到最紧急的任务
  3. 移除已执行的任务:从队列中删除任务

不同数据结构的性能对比:

数据结构插入获取最小值删除最小值
无序数组O(1)O(n)O(n)
有序数组O(n)O(1)O(1)
最小堆O(log n)O(1)O(log n)

最小堆在插入和删除操作上都有良好的性能,且获取最小值是O(1),非常适合任务调度场景。

什么是最小堆?

最小堆是一种完全二叉树,满足以下性质:

  1. 堆序性:每个节点的值都小于或等于其子节点的值
  2. 完全二叉树:除了最后一层,其他层都是满的,最后一层从左到右填充
最小堆示例:
        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) * 2

5.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);
}

插入步骤:

  1. 将新节点添加到数组末尾
  2. 执行"上浮"操作,维护堆序性

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;
}

移除步骤:

  1. 保存根节点(最小值)
  2. 将最后一个节点移到根位置
  3. 执行"下沉"操作,维护堆序性

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;
}

比较规则:

  1. 优先比较sortIndex(通常是过期时间)
  2. 如果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;

为什么使用位运算?

  1. 高效的合并操作:使用按位或(|)合并多个Lane
  2. 高效的检查操作:使用按位与(&)检查是否包含某个Lane
  3. 高效的移除操��:使用按位与非(&~)移除某个Lane
  4. 节省内存:一个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的问题

  1. 最小延迟setTimeout(fn, 0)实际延迟约4ms(浏览器限制)
  2. 嵌套限制:嵌套5层以上的setTimeout会被限制为4ms
  3. 优先级低setTimeout的优先级低于其他宏任务

MessageChannel的优势

  1. 无最小延迟:可以立即执行
  2. 无嵌套限制:不受嵌套层数影响
  3. 优先级适中:在宏任务队列中,优先级适中
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. 如果还有工作,回到步骤2

5.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的核心功能:

  1. 最小堆管理任务队列:高效地获取最高优先级任务
  2. 优先级调度:支持5种优先级级别
  3. 时间切片:使用shouldYieldToHost判断是否让出控制权
  4. 延迟任务:支持延迟执行的任务
  5. 任务中断与恢复:通过返回continuation实现

本章小结

本章深入探讨了React的调度系统,主要内容包括:

  1. Scheduler包概述:独立的调度器,与React解耦,可复用

  2. 任务优先级:5种优先级级别,从ImmediatePriority到IdlePriority,每种优先级有不同的超时时间

  3. 最小堆实现:使用最小堆管理任务队列,实现O(log n)的插入和删除,O(1)的查找最小值

  4. Lane优先级模型:使用32位整数表示优先级,通过位运算实现高效的合并、检查和分离操作

  5. 时间切片:将长任务分解成多个时间片,每个时间片默认5ms,使用MessageChannel实现调度

调度系统是React并发特性的基础。理解了调度系统,我们就能理解React如何在保证响应性的同时,高效地处理大量更新。

下一章,我们将学习协调过程,看看React如何使用Fiber架构和调度系统来计算UI的变化。


思考题

  1. 为什么React要实现自己的调度器,而不使用浏览器的requestIdleCallback

  2. 最小堆相比普通数组有什么优势?在什么场景下最小堆不是最优选择?

  3. Lane模型相比之前的ExpirationTime模型有什么优势?

  4. 时间切片的大小(5ms)是如何权衡的?如果改成1ms或50ms会有什么影响?

  5. 在你的项目中,哪些场景适合使用useTransition来启用时间切片?