React源码解析 - Scheduler

简述

React 的 scheduler 包是一个用于调度任务的库,它在 React 的并发模式和时间切片和管理任务的优先级功能中起着关键作用。

这个库帮助 React 管理任务的优先级,使得高优先级的任务(如用户输入)可以打断低优先级的任务(如后台数据同步),从而优化用户界面的响应性和性能。

核心任务:

  • 创建任务
  • 维护任务队列
  • 任务循环执行

简化版说明:

javascript 复制代码
import { unstable_scheduleCallback, PriorityLevel } from 'scheduler';

// 一个任务
function performWorkOnFiber(fiber) {
    // 这个函数模拟了更新任务的执行
    console.log("Updating component", fiber.id);
    // 这里会包含实际的组件更新逻辑
}

// 调度一个任务
unstable_scheduleCallback(PriorityLevel.Normal, performWorkOnFiber);

Output

只在package react-reconciler中使用,用到的方法及属性如下。

内部实现

核心变量和方法

维护连个个任务队列,每个任务都有一个到期时间。

ini 复制代码
////  核心全局变量
// Tasks are stored on a min heap
// 两种任务队列:小顶堆数据结构,按到期时间构建小顶堆,peek()获取最紧迫的任务
var taskQueue: Array<Task> = [];
var timerQueue: Array<Task> = [];

// Incrementing id counter. Used to maintain insertion order.
var taskIdCounter = 1;

// Pausing the scheduler is useful for debugging.
var isSchedulerPaused = false;

var currentTask = null;
var currentPriorityLevel = NormalPriority;

// This is set while performing work, to prevent re-entrance.
var isPerformingWork = false;

var isHostCallbackScheduled = false;
var isHostTimeoutScheduled = false;

核心方法 - 简化版本

scss 复制代码
//// 核心方法 - 简化版本
function workLoop(deadline) {
    // 检查是否应该让出控制权
    while (tasks.length > 0 && !shouldYieldToHost()) {
        const currentTask = tasks.shift(); // 获取任务队列中的下一个任务
        currentTask(); // 执行任务
        // 这里可以添加代码处理任务完成后的逻辑
    }

    if (tasks.length > 0) {
        // 如果任务队列中还有任务,继续调度 workLoop
        requestIdleCallback(workLoop);
    }
}

function shouldYieldToHost() {
    return deadline.timeRemaining() < 5; // 模拟 shouldYield,5毫秒作为阈值
}

// 模拟任务队列
const tasks = [    () => console.log("Task 1 completed"),    () => console.log("Task 2 completed"),    () => console.log("Task 3 completed")];

// 启动 workLoop
requestIdleCallback(workLoop);

任务队列:

scheduler管理两种任务队列,timerQueuetaskQueue

taskQueue vs timerQueue简介

timerQueuetaskQueue 是两种用于管理不同类型任务的队列,每个任务都有一个到期时间。它们各自承担不同的职责,但共同协作以确保应用的高效运行。了解这两种队列的关系可以帮助我们更好地理解 React 如何处理异步事件和调度任务。

TimerQueue

timerQueue 主要用于管理基于时间的任务,比如延迟执行的函数(例如使用 setTimeoutsetInterval 设置的定时器)。这些任务通常有一个特定的执行时间点,即它们不应该在设定的时间之前执行。

TaskQueue

这个是真正执行的任务队列,advanceTimers方法会检查timerQueue中到期的任务,然后更新到taskQueue

timerQueue 不同,taskQueue 通常用于存储即时执行的任务或那些需要尽快处理的任务,但不一定立即执行。这些任务可能包括 UI 更新、事件响应处理等。在 React 的上下文中,这些任务通常与组件的渲染和更新相关。

它们的关系

  1. 协同工作 :虽然 timerQueuetaskQueue 管理不同类型的任务,但它们在调度器中共同工作,以确保任务按适当的顺序和时间执行。例如,一个基于时间的任务可能需要在特定的时刻之后才能执行,而其他任务(如用户界面更新)可能需要尽快执行以保持应用的响应性。
  2. 优先级管理 :在一些实现中,这两种队列可能会根据任务的优先级进行处理。例如,即使 timerQueue 中的任务到时间了,如果 taskQueue 中有更高优先级的任务,调度器可能会先执行 taskQueue 中的任务。
  3. 执行策略 :调度器可能会根据当前的执行环境和性能要求选择不同的执行策略。例如,在一个宏任务(macro-task)完成后,调度器可能会检查 timerQueue 以确定是否有任何定时器到期,并在处理任何其他 taskQueue 任务之前执行这些到期的定时器任务。

实际应用

在 React 的具体实现中,调度器(如 React Scheduler)可能并不直接暴露 timerQueuetaskQueue 的具体操作,而是抽象出不同优先级的任务,并通过内部机制来决定何时以及如何执行这些任务。React 的并发模式(Concurrent Mode)利用了这种机制来允许长时间运行的任务被中断和恢复,从而不会阻塞主线程,影响用户的交互体验。

总的来说,timerQueuetaskQueue 的设计和实现使得任务调度更为灵活和高效,帮助开发者构建既能响应用户操作又能有效管理背后复杂逻辑的应用。

Task任务的数据结构

小顶堆,堆的根节点总是整个堆中的最小元素。也即最早需要处理的任务。也即peek方法,查找最小元素的操作是常数时间的 𝑂(1)。

typescript 复制代码
// Tasks are stored on a min heap
var taskQueue: Array<Task> = [];
var timerQueue: Array<Task> = [];

export opaque type Task = {
  id: number,
  callback: Callback | null,
  priorityLevel: PriorityLevel,
  startTime: number,
  expirationTime: number,
  sortIndex: number,
  isQueued?: boolean,
};

如何更新任务队列

unstable_scheduleCallback

职能:将任务插入到任务队列中,更新timerQueue或taskQueue

ini 复制代码
//* 将callback调度到任务队列,按需更新到timerQueue或taskQueue中
function unstable_scheduleCallback(
  priorityLevel: PriorityLevel,
  callback: Callback,
  options?: {delay: number},
): Task {
  var currentTime = getCurrentTime();

  var startTime;
  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;
  switch (priorityLevel) {
    case ImmediatePriority:
      // Times out immediately
      timeout = -1;
      break;
    case UserBlockingPriority:
      // Eventually times out
      timeout = userBlockingPriorityTimeout;
      break;
    case IdlePriority:
      // Never times out
      timeout = maxSigned31BitInt;
      break;
    case LowPriority:
      // Eventually times out
      timeout = lowPriorityTimeout;
      break;
    case NormalPriority:
    default:
      // Eventually times out
      timeout = normalPriorityTimeout;
      break;
  }

  var expirationTime = startTime + timeout;

  var newTask: Task = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime, // 更具delay来生成startTime,默认为currentTime
    expirationTime, // 根据不同优先级来生成该任务的expirationTime
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }

  // 如果是delay的任务,更新到timerQueue。否则更新到taskQueue。
  // 并注册任务执行,定时任务使用requestHostTimeout,taskQueue使用requestHostCallback
  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // Schedule a timeout.
      // 注册一个定时任务
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    // Schedule a host callback, if needed. If we're already performing work,
    // wait until the next time we yield.
    // 注册一个任务执行。如果当前有正在执行的任务,等待下一���时间窗口
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback();
      // -> schedulePerformWorkUntilDeadline -> performWorkUntilDeadline
      // -> flushWork -> workLoop -> schedulePerformWorkUntilDeadline
    }
  }

  return newTask;
}

advanceTimers

职能:检查timerQueue是否有到期的任务,并更新到taskQueue中

scss 复制代码
// ** 更新任务队列taskQueue:将timerQueue定时器队列里到期的任务添加到taskQueue任务队列中
function advanceTimers(currentTime: number) {
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}

任务循环 - workLoop

workLoop 是 React Scheduler 中的一个核心概念,用于连续处理任务队列中的任务。这个循环负责检查任务的优先级,决定执行哪些任务以及何时执行。在 React 的并发模式中,这个循环特别关键,因为它的设计允许 React 在执行更新过程中暂停和恢复工作从而不会阻塞主线程过长时间。

简化版

用数组的数据结构来简化实现任务队列,及其任务队列的循环执行。

scss 复制代码
// 假设的任务队列,实际中可能更复杂,如使用小顶堆等
const taskQueue = [];

// 任务添加函数
function scheduleTask(task) {
    taskQueue.push(task);
    taskQueue.sort((a, b) => a.priority - b.priority); // 简单排序,优先级低的在前
}

// 模拟的 workLoop
function workLoop(deadline) {
    // 当前时间切片还有剩余时间,允许执行任务
    while (taskQueue.length > 0 && deadline.timeRemaining() > 0) {
        const currentTask = taskQueue.shift(); // 获取并移除队列中的第一个任务
        currentTask.callback(); // 执行任务
    }
    // 当前时间切片没有剩余时间,注册到下个时间切片继续workloop
    if (taskQueue.length > 0) {
        // 如果还有任务未完成,请求浏览器再次调度 workLoop
        requestIdleCallback(workLoop);
    }
}

// 使用 requestIdleCallback 来启动 workLoop
requestIdleCallback(workLoop);

// 定义一些任务和它们的优先级
scheduleTask({ callback: () => console.log("Task 1 executed"), priority: 10 });
scheduleTask({ callback: () => console.log("Task 2 executed"), priority: 5 });
scheduleTask({ callback: () => console.log("Task 3 executed"), priority: 20 });

源码

workLoop

实际React的视线,任务队列是小顶堆的数据结构。该任务循环会循环执行的顶点, 直到被清空。

scss 复制代码
// ** 任务队列是小顶堆的数据结构。该任务循环会循环执行堆的顶点, 直到堆被清空。
function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue); // 获取当前任务
  // 注意实际workloop中是循环taskQueue,而非timerQueue。
  // 会在每个任务执行末尾去检查timerQueue重是否有到期的任务, 并插入到taskQueue

  // 循环执行任务: 循环执行堆的顶点, 直到堆被清空
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    // 1. 当前时间切片无剩余时间,或者有更高优先级的任务:break
    if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }

    // 2. 否则执行任务,并更新任务队列taskQueue和当前任务currentTask
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentTask.callback = null;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentPriorityLevel = currentTask.priorityLevel;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        // $FlowFixMe[incompatible-call] found when upgrading Flow
        markTaskRun(currentTask, currentTime);
      }
      const continuationCallback = callback(didUserCallbackTimeout); // TODO
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // If a continuation is returned, immediately yield to the main thread
        // regardless of how much time is left in the current time slice.
        // $FlowFixMe[incompatible-use] found when upgrading Flow
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskYield(currentTask, currentTime);
        }
        // 任务执行末尾去检查timerQueue重是否有到期的任务, 并插入到taskQueue
        advanceTimers(currentTime);
        return true;
      } else {
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskCompleted(currentTask, currentTime);
          // $FlowFixMe[incompatible-use] found when upgrading Flow
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        // 任务执行末尾去检查timerQueue重是否有到期的任务, 并插入到taskQueue
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

任务调度 - requestHostCallback

看到上面简化版本,有一个关键的方法requestIdleCallback。

React自己定义了一个类似requestIdleCallback功能的方法requestHostCallback :根据不同浏览器版本,分别用setImmediate,MessageChannel,setTimeout来实现。模拟的一个宏任务。

requestHostCallback 的核心功能是将一个回调函数排入宿主环境的任务队列中,以便在合适的时机执行。这个机制是实现时间切片和任务优先级管理的关键。

相关知识点:js事件循环机制

scss 复制代码
function handleTimeout(currentTime: number) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime); // 检查timerQueue并更新到taskQueue 

  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    } else {
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

// 可以理解成React按需封装的requestIdleCallback
function requestHostCallback() {
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

// 可以理解成React按需封装了一下的setTimeout
function requestHostTimeout(
  callback: (currentTime: number) => void,
  ms: number,
) {
  // $FlowFixMe[not-a-function] nullable value
  taskTimeoutID = localSetTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}

// *调度任务:由于performWorkUntilDeadline耗时,目的是将performWorkUntilDeadline注册到下一次类宏任务执中执行,优先执行下面的语句之后再执行该长时回调。
let schedulePerformWorkUntilDeadline;
// *类宏任务分3种情况:
// *REFER: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/setImmediate
// *1. 旧浏览器Node.js and old IE - 优先使用setImmediate,具有天然及时性,执行时机更符合预期
if (typeof localSetImmediate === 'function') {
  // Node.js and old IE.
  // There's a few reasons for why we prefer setImmediate.
  //
  // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
  // (Even though this is a DOM fork of the Scheduler, you could get here
  // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
  // https://github.com/facebook/react/issues/20756
  //
  // But also, it runs earlier which is the semantic we want.
  // If other browsers ever implement it, it's better to use it.
  // Although both of these would be inferior to native scheduling.
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  //* 2. 其它浏览器 - 其次用MessageChannel,优于setTimeout嵌套深度超过5级时会被限制在4ms
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  //* 3. NODE环境 - setTimeout
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = () => {
    // $FlowFixMe[not-a-function] nullable value
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

读后思考

  1. 任务队列为什么用小顶堆实现?为什么不用数组?
  2. requestHostCallback实现的具体功能是什么?如何调度任务的?(提示:js事件循环机制)
相关推荐
清汤饺子1 小时前
实践指南之网页转PDF
前端·javascript·react.js
霸气小男2 小时前
react + antDesign封装图片预览组件(支持多张图片)
前端·react.js
小白小白从不日白2 小时前
react 组件通讯
前端·react.js
小白小白从不日白4 小时前
react hooks--useReducer
前端·javascript·react.js
volodyan5 小时前
electron react离线使用monaco-editor
javascript·react.js·electron
等下吃什么?17 小时前
NEXT.js 创建postgres数据库-关联github项目-连接数据库-在项目初始化数据库的数据
react.js
小白小白从不日白19 小时前
react 高阶组件
前端·javascript·react.js
奶糖 肥晨1 天前
react是什么?
前端·react.js·前端框架
B.-2 天前
Remix 学习 - @remix-run/react 中主要的 hooks
前端·javascript·学习·react.js·web
盼盼盼2 天前
如何避免在使用 Context API 时出现状态管理的常见问题?
前端·javascript·react.js