【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 在浏览器主线程上能够 流畅渲染,不阻塞交互

相关推荐
ObjectX前端实验室2 小时前
【react18原理探究实践】调度器(Scheduler)原理深度解析
前端·react.js
路漫漫心远3 小时前
音视频学习笔记十八——图像处理之OpenCV检测
前端
摸着石头过河的石头3 小时前
从零开始玩转前端:一站式掌握Web开发基础知识
前端·javascript
sniper_fandc3 小时前
关于Mybatis-Plus的insertOrUpdate()方法使用时的问题与解决—数值精度转化问题
java·前端·数据库·mybatisplus·主键id
10岁的博客3 小时前
技术博客SEO优化全攻略
前端
南屿im4 小时前
别再被引用坑了!JavaScript 深浅拷贝全攻略
前端·javascript
想要一辆洒水车4 小时前
vuex4源码分析学习
前端
sophie旭4 小时前
一道面试题,开始性能优化之旅(6)-- 异步任务和性能
前端·javascript·性能优化
年少不知有林皇错把梅罗当球王4 小时前
vue2、vue3中使用pb(Base64编码)
前端