【react18原理探究实践】scheduler原理之Task 完整生命周期解析

🧑‍💻 前言

在上一篇文章里,我们从 scheduleUpdateOnFiber 出发,追踪到 React 内部调度的核心 ------ Scheduler

Scheduler 是 React 的"任务大脑",负责处理优先级、队列、时间切片和可中断渲染。

本篇文章我们将从 Task 的完整生命周期 出发,结合源码拆解,理解 Scheduler 的运作原理

1. Task 完整生命周期

先整体感受一下 Task 的"生老病死":

js 复制代码
Task 完整生命周期:
│
├─ 创建阶段
│  ├─ unstable_scheduleCallback(priority, callback)
│  ├─ 创建 newTask 对象
│  ├─ 计算 expirationTime
│  └─ push(taskQueue/timerQueue, newTask)
│
├─ 调度阶段  
│  ├─ requestHostCallback(flushWork)
│  ├─ MessageChannel.postMessage()
│  └─ 等待下一个宏任务
│
├─ 执行阶段
│  ├─ performWorkUntilDeadline()
│  ├─ flushWork() → workLoop()
│  ├─ peek(taskQueue) 获取最高优先级任务
│  │
│  └─ while 循环消费:
│     ├─ 检查中断条件 (shouldYieldToHost)
│     ├─ 执行 task.callback(didTimeout)
│     ├─ 处理 continuationCallback
│     └─ 移除已完成任务 pop(taskQueue)
│
├─ 中断阶段 (可选)
│  ├─ shouldYieldToHost() 返回 true
│  ├─ break 退出 workLoop
│  └─ 返回 hasMoreWork = true
│
└─ 恢复阶段 (可选)
   ├─ 续接函数保留在 currentTask.callback
   ├─ 重新调度 port.postMessage()
   └─ 下一个时间片继续执行

一句话总结:Task 的生命周期 = 创建 → 调度 → 执行 → 中断/恢复

2. Task 创建阶段

2.1 入口函数:unstable_scheduleCallback

创建任务的入口是 unstable_scheduleCallback

js 复制代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = getCurrentTime();

  // 计算任务的开始时间
  var startTime = currentTime;
  if (options && typeof options.delay === 'number' && options.delay > 0) {
    startTime = currentTime + options.delay;
  }

  // 根据优先级确定超时时间
  var timeout;
  switch (priorityLevel) {
    case ImmediatePriority: timeout = -1; break;
    case UserBlockingPriority: timeout = 250; break;
    case LowPriority: timeout = 10000; break;
    case IdlePriority: timeout = IDLE_PRIORITY_TIMEOUT; break;
    case NormalPriority:
    default: timeout = 5000; break;
  }

  var expirationTime = startTime + timeout;

  // 构建任务对象
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };

  // 判断任务进入哪个队列
  if (startTime > currentTime) {
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    requestHostTimeout(handleTimeout, startTime - currentTime);
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    requestHostCallback(flushWork);
  }

  return newTask;
}

2.2 Task 对象结构

js 复制代码
var newTask = {
  id: taskIdCounter++,     // 唯一任务ID
  callback,                // 要执行的回调函数
  priorityLevel,           // 优先级
  startTime,               // 任务开始时间
  expirationTime,          // 过期时间
  sortIndex: expirationTime// 堆排序依据
};

2.3 原理解析

Scheduler 的核心目标是:把任务拆成小块,分时间片执行,保证 UI 不被长任务卡住

在任务创建阶段:

  1. 计算 startTime :如果有 delay,任务是延迟执行的,放入 timerQueue

  2. 计算 expirationTime:不同优先级对应不同超时时间:

    • Immediate:马上执行
    • UserBlocking:250ms 内完成
    • Normal:5000ms
    • Low:10000ms
    • Idle:空闲时执行
  3. 放入队列

    • 延迟任务 → timerQueue
    • 可立即执行任务 → taskQueue
  4. 调度触发

    • requestHostCallback(flushWork) → 浏览器下一个宏任务触发执行
    • 延迟任务设置定时器 → 时间到移入 taskQueue

3. 队列管理

Scheduler 内部维护两个最小堆:

js 复制代码
var taskQueue = [];  // 按 expirationTime 排序
var timerQueue = []; // 按 startTime 排序

3.1 延迟任务转移机制

当时间到达时,延时任务会从 timerQueue 移入 taskQueue

js 复制代码
function advanceTimers(currentTime) {
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      pop(timerQueue); // 已取消
    } else if (timer.startTime <= currentTime) {
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
    } else {
      return; // 还没到时间
    }
    timer = peek(timerQueue);
  }
}

📌 原理

延迟任务不会阻塞主线程,时间到后才进入执行队列。


4. Task 执行阶段

4.1 主循环:workLoop

js 复制代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);

  while (currentTask !== null) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }

    const callback = currentTask.callback;
    currentTask.callback = null;

    const didTimeout = currentTask.expirationTime <= currentTime;
    const continuationCallback = callback(didTimeout);
    currentTime = getCurrentTime();

    if (typeof continuationCallback === 'function') {
      currentTask.callback = continuationCallback;
    } else {
      if (currentTask === peek(taskQueue)) {
        pop(taskQueue);
      }
    }
    advanceTimers(currentTime);
    currentTask = peek(taskQueue);
  }

  return currentTask !== null;
}

4.2 执行逻辑总结

  1. peek(taskQueue) 取最高优先级任务
  2. 检查是否超时、是否需要让出主线程
  3. 执行 task.callback(didTimeout)
  4. 有续接函数 → 保存回调,下次继续
  5. 无续接函数 → 任务完成,移出队列

📌 原理

时间切片机制(默认 5ms)保证浏览器主线程不会被长任务阻塞。


5. Task 中断机制

中断点在 workLoop

js 复制代码
if (
  currentTask.expirationTime > currentTime &&
  (!hasTimeRemaining || shouldYieldToHost())
) {
  break; // 中断
}

5.1 shouldYieldToHost

js 复制代码
function shouldYieldToHost() {
  const currentTime = getCurrentTime();
  if (currentTime >= deadline) {
    if (needsPaint || (navigator.scheduling && navigator.scheduling.isInputPending())) {
      return true;
    }
    return currentTime >= maxYieldInterval; // 最多 300ms
  }
  return false;
}

📌 原理

  • 每个时间片最多 5ms(短任务)
  • 遇到用户交互或绘制请求立即中断
  • 避免长任务阻塞 UI

6. Task 恢复机制

如果任务没完成,回调会返回一个 续接函数

js 复制代码
const continuationCallback = callback(didTimeout);
if (typeof continuationCallback === 'function') {
  currentTask.callback = continuationCallback;
}

Scheduler 会在下一个时间片继续执行。

React 渲染任务正是利用这个机制实现 可中断渲染

js 复制代码
function performConcurrentWorkOnRoot(root, didTimeout) {
  if (workInProgress !== null) {
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

7. 取消机制

取消任务非常简单:

js 复制代码
function unstable_cancelCallback(task) {
  task.callback = null;
}

workLoop 执行时会跳过被取消的任务。


8. 完整时序图

js 复制代码
用户调用 unstable_scheduleCallback
│
├─ 创建 Task
│   ├─ 计算 startTime / expirationTime
│   └─ 放入 taskQueue / timerQueue
│
├─ 调度
│   └─ requestHostCallback(flushWork)
│       └─ MessageChannel.postMessage()
│
├─ 宿主环境触发 performWorkUntilDeadline
│   └─ flushWork()
│       └─ workLoop()
│           ├─ advanceTimers()
│           ├─ 取出 taskQueue 队头
│           ├─ shouldYieldToHost → 时间切片判断
│           ├─ 执行 callback(didTimeout)
│           ├─ continuationCallback → 保存续接
│           └─ pop 已完成任务
│
├─ 中断 (可选)
│   └─ shouldYieldToHost 返回 true → break
│
└─ 恢复 (可选)
    ├─ 续接函数保存在 currentTask.callback
    ├─ 再次 requestHostCallback
    └─ 下一个时间片继续执行

✅ 总结

Scheduler 的核心机制:

  1. 任务优先级:Immediate > UserBlocking > Normal > Low > Idle
  2. 双队列管理:taskQueue + timerQueue
  3. 时间切片:默认 5ms,最大 300ms
  4. 可中断/恢复:续接函数机制
  5. 任务取消:callback = null

这套机制保证 React 在浏览器主线程上能够 流畅渲染,不阻塞交互

相关推荐
chao_6666661 天前
React Native + Expo 开发指南:编译、调试、构建全解析
javascript·react native·react.js
前端一课1 天前
分享:基于Next.js的企业级提示词AI平台
前端
小高0071 天前
🔥「从零到一」我用 Node BFF 手撸一个 Vue3 SSR 项目(附源码)
前端·javascript·vue.js
SailingCoder1 天前
AI 流式对话该怎么做?SSE、fetch、axios 一次讲清楚
前端·javascript·人工智能·ai·node.js
hxjhnct1 天前
Vue 实现多行文本“展开收起”
前端·javascript·vue.js
橙子的AI笔记1 天前
2025年全球最受欢迎的JS鉴权框架Better Auth,3分钟带你学会
前端·ai编程
百锦再1 天前
Vue大屏开发全流程及技术细节详解
前端·javascript·vue.js·微信小程序·小程序·架构·ecmascript
独自破碎E1 天前
你知道Spring Boot配置文件的加载优先级吗?
前端·spring boot·后端
一树山茶1 天前
Vue变化响应
前端