深入React19任务调度器Scheduler

Scheduler = 最小堆 + MessageChannel + 时间片检查

它的目标只有一个:推进任务,但永远别饿死浏览器。

调度任务

React 19 最新 Scheduler 源码**(packages/scheduler)** 中,普通任务(非 Immediate,同步优先级之外的任务)的完整调度链:

Plain 复制代码
unstable_scheduleCallback
  ↓
requestHostCallback
  ↓
schedulePerformWorkUntilDeadline
  ↓
performWorkUntilDeadline
  ↓
flushWork
  ↓
workLoop
  ↓
shouldYieldToHost
  ↓
advanceTimers

Scheduler 本质是一个"带时间片控制的最小堆 + 宏任务驱动循环"调度器。

一、调度的起点:unstable_scheduleCallback

源码位置(React 19):

Plain 复制代码
/packages/scheduler/src/forks/Scheduler.js
TypeScript 复制代码
let getCurrentTime = () => performance.now();

// 为所有任务维护了连个最小堆(每次从队列里面取出来的都是优先级最高(时间即将过期))
// timerQueue     // 延时任务队列(task.startTime > now) ------ 按 startTime 排序(延迟最小的先看)
// taskQueue      // 立即可执行的任务(task.startTime <= now)------ 按 expirationTime 排序(最紧急的先执行)

// Timeout 对应的值
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // 1073741823

/**
​ * 调度任务的入口
​ * ​@param​ {*} priorityLevel 优先级等级
​ * ​@param​ {*} callback 任务回调函数
​ * ​@param​ {*} options { delay: number } 该对象有 delay 属性,表示要延迟的时间(决定 expirationTime)
​ * ​@returns
​ */
 function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前的时间
  var currentTime = getCurrentTime();

  var startTime;
  // 设置起始时间 startTime:如果有延时 delay,起始时间需要添加上这个延时,否则起始时间就是当前时间
  if (typeof options === "object" && options !== null) {
    var delay = options.delay;
    if (typeof delay === "number" && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  var timeout;
  // 根据传入的优先级等级来设置不同的 timeout
  switch (priorityLevel) {
    case ImmediatePriority:
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT; // 10000
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
      break;
  }
  // 接下来就计算出过期时间
  // 只有 ImmediatePriority 任务 比当前时间要早,其他任务都会不同程度的延迟
  var expirationTime = startTime + timeout;

  // 创建一个新的任务
  var newTask = {
    id: taskIdCounter++, // 任务 id
    callback, // 执行任务回调函数 export type Callback = boolean => ?Callback;
    priorityLevel, // 任务的优先级
    startTime, // 任务开始时间
    expirationTime, // 任务的过期时间
    sortIndex: -1, // 用于小顶堆优先级排序,始终从任务队列中拿出最优先的任务
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  if (startTime > currentTime) {
    // 说明这是一个延时任务
    newTask.sortIndex = startTime;
    // 将该任务推入到 timerQueue 的任务队列中
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // 说明 taskQueue 里面的任务已经全部执行完毕,然后从 timerQueue 里面取出一个优先级最高的任务作为此时的 newTask
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 如果是延时任务,调用 requestHostTimeout 进行延时任务的调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 说明不是延时任务
    newTask.sortIndex = expirationTime; // 设置了 sortIndex 后,在任务队列里面进行一个排序
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // 最终调用 requestHostCallback 进行普通任务的调度
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    }
  }

  // 向外部返回任务
  return newTask;
}

它的职责是:

  1. 根据优先级计算 timeout
  2. 构造一个 Task 对象
  3. 放入任务到对应的最小堆
  4. 请求宿主开始调度(requestHostCallback 普通任务 / requestHostTimeout 延时任务)

任务执行时刻:

IMMEDIATE_PRIORITY_TIMEOUT -> currentTime -> USER_BLOCKING_PRIORITY_TIMEOUT -> IDLE_PRIORITY_TIMEOUT

二、普通任务调用 requestHostCallback

当普通任务进入 taskQueue 后,调用 requestHostCallback,然后调用 schedulePerformWorkUntilDeadline

TypeScript 复制代码
/**
​ * 
​ * ​@param​ ​{*}​ callback 是在调用的时候传入的 flushWork
​ * requestHostCallback 这个函数没有做什么事情,主要就是调用 schedulePerformWorkUntilDeadline
​ */
 function requestHostCallback() {
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();// 实例化 MessageChannel 进行后面的调度
  }
}

let schedulePerformWorkUntilDeadline; // undefined
if (typeof localSetImmediate === 'function') {
  // Node.js and old IE.
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // 大多数情况下,使用的是 MessageChannel
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // setTimeout 进行兜底
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

schedulePerformWorkUntilDeadline 根据不同的环境选择不同的生成宏任务的方式。大多数都是 MessageChannel :

  • 每一次调度推进
  • 都是一个新的宏任务
  • 浏览器中间有机会 paint

三、延时任务调用 requestHostTimeout & handleTimeout

React Scheduler 不直接使用 setTimeout,而是抽象成 HostConfig,可在不同环境实现。

requestHostTimeout(callback, ms) 这个函数会安排一个底层超时回调。

在浏览器环境下它一般等价于:

JavaScript 复制代码
const timeoutID = setTimeout(callback, ms);

注意:这是 ​严格意义上的延时调度​,用于把 timerQueue 的任务唤醒进 taskQueue。

requestHostTimeout 实际上就是调用 setTimoutout,然后在 setTimeout 中,调用传入的 handleTimeout。

注意:延时任务只需要"不会提前执行",而不需要"精准执行",所以这里使用了 setTimeout,setTimeout 只是负责"唤醒" Scheduler,不负责精度和优先级(expirationTime 控制)控制,鉴于负责延时和必须是宏任务的特性,这里使用 setTimeout 最合适。

React 的调度精度来自:MessageChannel + 时间片检查 + expirationTime 。

handleTimeout 是真正被 setTimeout 调用的函数:

TypeScript 复制代码
function handleTimeout(currentTime) {
  // 遍历 timerQueue,将时间已经到了的延时任务放入到 taskQueue
  advanceTimers(currentTime);
  if (!isHostCallbackScheduled) {
    if (taskQueue.length > 0) {
      // 采用调度普通任务的方式进行调度
      requestHostCallback(flushWork);
    } else {
      // 如果任务仍然是延时,继续设置 HostTimeout
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        const nextDelay = firstTimer.startTime - currentTime;
        requestHostTimeout(handleTimeout, nextDelay);
      }
    }
  }
}

四、performWorkUntilDeadline

这是 Scheduler 的"驱动心跳"。

TypeScript 复制代码
let startTime = -1;
const performWorkUntilDeadline = () => {
  if (enableRequestPaint) {
    needsPaint = false;
  }
  if (isMessageLoopRunning) {
    const currentTime = getCurrentTime();
    // 这里的 startTime 并非 unstable_scheduleCallback 方法里面的 startTime
    // 而是一个全局变量,默认值为 -1
    // 用来测量任务的执行时间,从而能够知道主线程被阻塞了多久
    startTime = currentTime;
    let hasMoreWork = true;
    try {
      // flushWork 为任务中转,本质上内部继续调用 workLoop 判断任务执行情况
      hasMoreWork = flushWork(currentTime);
    } finally {
      if (hasMoreWork) {
        // 上面刚刚讲过的方法,根据不同的环境选择不同的生成宏任务的方式(MessageChannel)
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
      }
    }
  }
};

它做三件事:

  1. 设置本次调度的时间起点
  2. 调用 flushWork
  3. 如果还有任务 → 再发一个 MessageChannel

五、flushWork 和 workLoop 执行任务循环

TypeScript 复制代码
// flushWork 是个中转,它真正执行的是  workLoop
function flushWork(initialTime) {
  // ...
  // 各种开关、报错捕获...
  // 其实核心就是 workLoop
  return workLoop(initialTime);
}

// 不断从任务队列中取出任务执行
function workLoop(initialTime: number) {
  // initialTime 开始执行任务的时间
  let currentTime = initialTime;
  // advanceTimers 是用来遍历 timerQueue,判断是否有已经到期的任务
  // 如果有,将这个任务放入到 taskQueue
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (!enableAlwaysYieldScheduler) {
      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
        // 任务没有过期,并且需要中断任务,归还主线程
        break;
      }
    }
    // 返回任务本身,致使任务之后可以接着继续执行
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        markTaskRun(currentTask, currentTime);
      }
      // 执行任务,其他判断都是做"阶段过程资料收集",无需关注
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          markTaskYield(currentTask, currentTime);
        }
        advanceTimers(currentTime);
        return true;
      } else {
        if (enableProfiling) {
          markTaskCompleted(currentTask, currentTime);
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    // 执行完,再从 taskQueue 中取出一个任务
    currentTask = peek(taskQueue);
    if (enableAlwaysYieldScheduler) {
      if (currentTask === null || currentTask.expirationTime > currentTime) {
        break;
      }
    }
  }
  // 如果任务不为空,那么还有更多的任务,hasMoreTask 为 true
  if (currentTask !== null) {
    return true;
  } else {
    // taskQueue 这个队列空了,那么我们就从 timerQueue 里面去看延时任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    // 没有进入上面的 if,说明 timerQueue 里面的任务也完了,返回 false,外部的 hasMoreWork 拿到的就也是 false
    return false;
  }
}

任务是否可以被打断?

TypeScript 复制代码
shouldYieldToHost()

如果返回 true:

  • 当前宏任务结束
  • 重新发 MessageChannel
  • 浏览器可以渲染

六、shouldYieldToHost ------ 时间片控制核心

源码核心:

TypeScript 复制代码
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;

  if (timeElapsed < frameInterval) {
    return false;
  }

  return true;
}

其中:

TypeScript 复制代码
frameInterval = 5ms

注意:

React 不等 16ms 一整帧

而是:

每 5ms 检查一次

为什么?

  • 预留给浏览器 layout + paint
  • 预留给输入事件

七、advanceTimers ------ 延迟任务晋升机制

每次 workLoop 结束后,会调用 advanceTimers(currentTime);

它会:

  • 检查 timerQueue
  • 把到时间的任务移动到 taskQueue
TypeScript 复制代码
function advanceTimers(currentTime) {
  // 取出 startTime <= currentTime 的 timer
  let timer = peek(timerQueue);
  while (timer !== null && timer.startTime <= currentTime) {
    pop(timerQueue);
    timer.sortIndex = timer.expirationTime;
    push(taskQueue, timer);
    timer = peek(timerQueue);
  }
}

这保证:延迟任务不会饿死

八、Scheduler 与 Fiber 的关系

Scheduler 不知道 Fiber。

它只负责:"什么时候调用 callback"

而 callback 通常是:

Plain 复制代码
performConcurrentWorkOnRoot

那里面才是:

  • beginWork
  • completeWork
  • commitRoot

九、任务拆分的思路来源

假设有这样一段源码:

JavaScript 复制代码
unstable_scheduleCallback(NormalPriority, () => {
  heavyWork(); // 10ms
});

如果 heavyWork() 是同步执行 10ms:,Scheduler ​无法中断它,​因为 JS 是单线程的,函数执行过程中无法被抢占。

来看源码核心逻辑:

JavaScript 复制代码
const continuation = callback();

if (typeof continuation === 'function') {
  currentTask.callback = continuation;
} else {
  pop(taskQueue);
}

如果 callback 返回一个函数,Scheduler 会把它当成"任务的下一段"。

举一个最小可理解示例:

JavaScript 复制代码
// 假设这是一个"大"任务
function createBigTask(total) {
  let i = 0;

  function work() {
    while (i < total) {
      console.log(i++);
      
      if (shouldStopManually()) {
        return work; // 👈 返回自身,表示还有后续
      }
    }

    return null; // 完成
  }

  return work;
}

调度:

JavaScript 复制代码
unstable_scheduleCallback(
  NormalPriority,
  createBigTask(1000)
);

执行:

Plain 复制代码
workLoop()
  ↓
执行 work()
  ↓
执行到一部分
  ↓
返回 work (continuation)
  ↓
Scheduler 保存 callback = work
  ↓
shouldYieldToHost() 为 true
  ↓
break
  ↓
下一帧继续执行

在 React 里,被拆分的任务不是"普通函数",而是:performConcurrentWorkOnRoot。

它内部会调用:workLoopConcurrent() 。

核心逻辑(简化版):

JavaScript 复制代码
while (
  workInProgress !== null &&
  !shouldYield()
) {
  performUnitOfWork(workInProgress);
}

performUnitOfWork 只做一个 Fiber 节点:

JavaScript 复制代码
function performUnitOfWork(unit) {
  const next = beginWork(unit);

  if (next === null) {
    completeUnitOfWork(unit);
  }

  return next;
}

也就是说:React 把整棵 Fiber 树拆成一个个"节点单位"/"任务切片"。

更形象的可以表示为:

一个组件树:

Plain 复制代码
App
 ├─ Header
 ├─ Content
 │    ├─ List
 │    └─ Sidebar
 └─ Footer

会被拆成:

Plain 复制代码
performUnitOfWork(App)
performUnitOfWork(Header)
performUnitOfWork(Content)
performUnitOfWork(List)
performUnitOfWork(Sidebar)
performUnitOfWork(Footer)

每个 Fiber 节点就是一个小任务。

回到 Scheduler 这段判断:

JavaScript 复制代码
if (
  currentTask.expirationTime > currentTime &&
  shouldYieldToHost()
) {
  break;
}

当时间片用完时:break 。

此时:

  • workLoop 停止
  • 任务还没完成
  • currentTask.callback 仍然是 performConcurrentWorkOnRoot

下一帧:

Plain 复制代码
MessageChannel
  ↓
performWorkUntilDeadline
  ↓
flushWork
  ↓
workLoop
  ↓
继续执行 performConcurrentWorkOnRoot

延时任务并不会立刻进入 taskQueue,而是先进入 timerQueue,等待被"唤醒"然后再调度。

十、完整调度流程

下面是一个完整的流程图:

Plain 复制代码
unstable_scheduleCallback()
            ↓
            是否延时? (startTime > now)
           / \
        是     否
         ↓      ↓
  push(timerQueue) push(taskQueue)
         ↓      ↓
requestHostTimeout(requestHostCallback)
         ↓      ↓
       等待 Timer   requestHostCallback
         ↓              ↓
timeout 到时           MessageChannel
  ↓                   ↓
handleTimeout       performWorkUntilDeadline
  ↓                   ↓
advanceTimers         flushWork
  ↓                  ↓
timerQueue → taskQueue
                    ↓
                 workLoop
                    ↓
            shouldYieldToHost?
                    ↓
       是                      否
 notifyBrowserPaint          执行 Callback
                             ↓
                       callback 返回 continuation?
                             ↓
            是                               否
    更新 Task.callback                      pop(taskQueue)
                             ↓
                     可能再次调度

示例理解

示例 1:普通任务(正常进入队列)

JavaScript 复制代码
unstable_scheduleCallback(
  NormalPriority,
  () => console.log("normal")
);

步骤:

  1. now <= startTime (0 delay)
  2. 放入 taskQueue
  3. requestHostCallback(flushWork)
  4. MessageChannel 触发 performWorkUntilDeadline
  5. flushWork → workLoop
  6. 执行 task
  7. taskQueue 空 → 调度结束

示例 2:延时任务(未来才执行)

JavaScript 复制代码
unstable_scheduleCallback(
  NormalPriority,
  () => console.log("delayed"),
  { delay: 1000 }
);

步骤:

Plain 复制代码
t=0
↓
startTime = now + 1000
push(timerQueue)
requestHostTimeout(handleTimeout, 1000)

1s 后:

Plain 复制代码
handleTimeout 被 setTimeout 调用
↓
advanceTimers
  → 取出 timerQueue 里所有 startTime <= 1s 的任务
  → 推到 taskQueue
taskQueue 变非空
↓
requestHostCallback(flushWork)
↓
MessageChannel 回调
↓
flushWork → workLoop → task 执行

示例 3:延时任务到了中间又被打断

JavaScript 复制代码
unstable_scheduleCallback(
  NormalPriority,
  step1,
  { delay: 1000 }
);

function step1() {
  console.log("step1");
  return step2;
}

function step2() {
  console.log("step2");
}

执行过程:

Plain 复制代码
t=0 → timerQueue 入队
t=1000  → handleTimeout
  → advanceTimers → taskQueue
↓
MessageChannel
↓
workLoop 执行 step1 延时任务
↓
step1 返回 continuation step2 延时任务
↓
此时
  workLoop 检查 shouldYieldToHost
  如果 time片到 → 中断
  → callback (continuation) 留在 taskQueue
  → requestHostCallback 执行普通任务
下次继续执行 step2 延时任务

设计哲学

① 延时任务不能抢占浏览器渲染

如果立即调度:

Plain 复制代码
setTimeout(() => {}), Promise.then(...)

React 可能会阻塞页面渲染

所以必须分开:

Plain 复制代码
先 timerQueue 等待
再 taskQueue 才跑

② 延时任务按照优先级

即使 delay 到了:

Plain 复制代码
expirationTime = startTime + timeout

如果更高优先级任务进入 taskQueue:

React 会先执行高优先级的。

delay 控制什么时候进入 taskQueue,而 expirationTime 控制进入 taskQueue 之后的优先级排序。

③ extendable 继续推进任务

一个任务返回 continuation 时:

Plain 复制代码
currentTask.callback = continuation;

并且还可能继续 schedule。

相关推荐
一枚前端小姐姐1 小时前
Vue3 + Pinia 状态管理,从入门到模块化
前端·vue.js
boooooooom1 小时前
Vue3 nextTick 实现大变化:微任务优先,彻底搞懂渲染时机!
javascript·vue.js·面试
用户14436183400971 小时前
你不知道的JS上-(九)
前端·javascript
yuki_uix2 小时前
为什么我的 Auth Token 藏在了 Network 面板的 Doc 里?
前端·python·debug
不会敲代码12 小时前
从原子CSS到TailwindCSS:现代前端样式解决方案全解析
前端·css·react.js
Wect2 小时前
LeetCode 102. 二叉树的层序遍历:图文拆解+代码详解
前端·算法·typescript
简离2 小时前
VSCode Git Bash 终端:告别内置vi,直接用VSCode编辑交互内容
前端
冴羽2 小时前
2026 年 JavaScript 框架 3 大趋势
前端·javascript·react.js
思茂信息2 小时前
基于CST 3D Combined功能的以太网口RE仿真
开发语言·javascript·单片机·嵌入式硬件·matlab·3d