【前端架构和框架】react中Scheduler调度原理

本篇我们看下调度器部分,本系列文章旨在分析react核心模块内部运行原理和源码实现,提升架构和编码能力。

一、调度器核心功能

调度器最核心的功能就是:

  • 异步渲染
  • 时间切片
  • 优先级调度

二、调度器(Scheduler)整体运行流程

调度器的核心目标是:根据任务优先级,合理分配浏览器资源,高优任务优先执行,低优任务可延迟 / 中断。整体流程如下:

  1. 提交任务 :通过 scheduleCallback(priority, callback) 提交任务,传入优先级和执行函数。

  2. 计算过期时间:根据优先级计算任务的「过期时间」(如用户交互任务 250ms 后过期)。

  3. 入队排序:任务被插入小顶堆队列,按过期时间排序,堆顶为最高优任务。

  4. 调度执行

    • 高优任务:同步执行,不设时间限制(确保用户操作快速响应)。
    • 低优任务:请求浏览器空闲时间,每次执行不超过 5ms。
  5. 任务续行:若低优任务在时间切片内未完成,返回「续行回调」,下次调度时恢复执行。

  6. 抢占逻辑:新高优任务插入时,中断当前低优任务,优先执行高优任务。

三、核心阶段源码分析

React 调度器源码位于 scheduler 包(packages/scheduler/src/Scheduler.js),核心阶段包括任务提交优先级计算队列管理任务执行

1. 任务提交阶段(scheduleCallback

scheduleCallback 是调度器入口,负责创建任务并加入队列。

javascript 复制代码
 
function unstable_scheduleCallback(priorityLevel, callback) {
  // 1. 计算任务过期时间(核心:优先级转换为时间)
  const currentTime = getCurrentTime(); // 当前时间(ms)
  let timeout;
  switch (priorityLevel) {
    case ImmediatePriority: // 同步优先级(最高)
      timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
      break;
    case UserBlockingPriority: // 用户交互优先级(高)
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250ms
      break;
    case NormalPriority: // 正常优先级(中)
      timeout = NORMAL_PRIORITY_TIMEOUT; // 5000ms
      break;
    case LowPriority: // 低优先级
      timeout = LOW_PRIORITY_TIMEOUT; // 10000ms
      break;
    case IdlePriority: // 空闲优先级(最低)
      timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823ms(约34年)
      break;
  }
  const expirationTime = timeout > 0 ? currentTime + timeout : timeout;

  // 2. 创建任务对象
  const newTask = {
    id: taskIdCounter++,
    callback, // 任务执行函数
    priorityLevel,
    expirationTime,
    startTime: currentTime, // 任务开始时间
    sortIndex: -1, // 排序索引(用于堆排序)
  };

  // 3. 按优先级插入任务队列(小顶堆)
  if (enableProfiling) {
    // 性能监控逻辑(略)
  }
  newTask.sortIndex = expirationTime; // 按过期时间排序
  push(taskQueue, newTask);

  // 4. 调度任务执行
  if (!isHostCallbackScheduled && !isPerformingWork) {
    isHostCallbackScheduled = true;
    requestHostCallback(flushWork); // 请求执行任务
  }

  return newTask; // 返回任务标识,用于取消任务
}

核心逻辑

  • scheduleCallback和unstable_scheduleCallback是同一个函数,在协调那边调用,注册一个带优先级的performConcurrentWorkOnRoot任务。前面一篇文中react工作循环
  • 将「优先级等级」转换为「过期时间」(优先级越高,过期时间越早)。
  • 任务队列采用小顶堆结构,确保每次能快速取出「最早过期」的任务(最高优)。
  • requestHostCallback启动一个宏任务,可以看下面的源码部分。它既然把flushWork作为入参,那么任务的执行者 本质上调用的就是flushWork。flushWork也在下文中

2. 任务调度阶段(requestHostCallback

requestHostCallback 负责向浏览器请求执行时机,低优任务利用空闲时间,高优任务同步执行。

ini 复制代码
// 简化版源码
let isMessageLoopRunning = false;
const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const hasTimeRemaining = true;
    // scheduledHostCallback去执行任务的函数,
    // 当任务因为时间片被打断时,它会返回true,表示
    // 还有任务,所以会再让调度者调度一个执行者
    // 继续执行任务
    const hasMoreWork = scheduledHostCallback(
      hasTimeRemaining,
      currentTime,
    );

    if (!hasMoreWork) {
      // 如果没有任务了,停止调度
      isMessageLoopRunning = false;
      scheduledHostCallback = null;
    } else {
      // 如果还有任务,继续让调度者调度执行者,便于继续
      // 完成任务
      port.postMessage(null);
    }

  } else {
    isMessageLoopRunning = false;
  }
};

 
 const channel = new MessageChannel();
 const port = channel.port2;
 channel.port1.onmessage = performWorkUntilDeadline;
  
  requestHostCallback = function(callback) {
    scheduledHostCallback = callback;
    if (!isMessageLoopRunning) {
      isMessageLoopRunning = true;
      port.postMessage(null);
    }
  };
 

核心逻辑

  • 低优任务通过 MessageChannel 延迟执行,每次执行不超过 5ms(时间切片)。
  • 若任务在时间切片内未完成,会保存状态并请求下一次执行(实现 "可中断")。
  • port.postMessage(null)发起后,回调函数performWorkUntilDeadline会执行,上面scheduledHostCallback = callback被赋值,callback就是上面传入的flushWork函数。scheduledHostCallback == flushWork,所以performWorkUntilDeadline执行就是执行flushWork。

3. 任务执行阶段(flushWork

flushWork 是任务执行的核心函数,负责从队列中取出最高优任务并执行。

ini 复制代码
 
// 省略无关代码
function flushWork(hasTimeRemaining, initialTime) {
  // 1. 做好全局标记, 表示现在已经进入调度阶段
  isHostCallbackScheduled = false;
  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    // 2. 循环消费队列
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    // 3. 还原全局标记
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
  }
}

// 省略部分无关代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
  currentTask = peek(taskQueue); // 获取队列中的第一个任务
  while (currentTask !== null) {
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 执行回调
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      // 回调完成, 判断是否还有连续(派生)回调
      if (typeof continuationCallback === 'function') {
        // 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
        currentTask.callback = continuationCallback;
      } else {
        // 把currentTask移出队列
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
    } else {
      // 如果任务被取消(这时currentTask.callback = null), 将其移出队列
      pop(taskQueue);
    }
    // 更新currentTask
    currentTask = peek(taskQueue);
  }
  if (currentTask !== null) {
    return true; // 如果task队列没有清空, 返回ture. 等待调度中心下一次回调
  } else {
    return false; // task队列已经清空, 返回false.
  }
}

workLoop循环是核心,下面我们来分析下,workLoop函数中currentTask.callback其实就是performConcurrentWorkOnRoot即render阶段入口,通过深度优先去构建fiber树,每个fiber节点是一个执行单元(前一篇文章中两大循环之一的fiber树构建循环)。

每一次while循环的退出就是一个时间切片, 深入分析while循环的退出条件:

  1. 队列被完全清空: 这种情况就是很正常的情况, 一气呵成, 没有遇到任何阻碍.

  2. 超时检查(时间切片用尽): 分两种情况,一个是每次调度任务时,这个是在上面workLoop函数中,每次调度一个新任务前去做时间分片 检查;另一个是如果某个task.callback执行时间太长(如: fiber树很大)也会造成超时,每次构建一个工作单元fiber节点的时候也要去检查。所以两种情况都会时间片耗尽退出。我们分开分析

  3. 一个长任务超时,这种情况是fiber太大了,只构建了一部分,若时间切片用尽(shouldYieldToHost() 返回 true),则暂停执行,等待下次调度。这个时候会协调器那边返回一个新的performConcurrentWorkOnRoot(保存上下文节点),上面代码const continuationCallback = callback(didUserCallbackTimeout)这时continuationCallback等于performConcurrentWorkOnRoot,这个时候任务不从队列中移除,下一帧时间切片继续从中断出执行(实现可恢复)。

  4. 如果是多个任务间让出,这个就是下一帧时间切片选择堆顶任务继续执行(实现让出主线程,不阻塞)。

  5. 如果这个时候突然来一个优先级高的任务比如用户点击事件,这个时候会取消当前的正在执行的低优先级任务,先调用高优先级任务,执行完后,再重新调用低优先级任务(实现高优先级打断低优先级)。

4. 时间分片

shouldYieldToHost 函数在每次任务间调度(多个短任务在一个时间片内调度)和一个长任务内(超过5ms,执行fiber构建)调用,都要判断是否需要中断。

js 复制代码
const localPerformance = performance;
// 获取当前时间
getCurrentTime = () => localPerformance.now();

// 时间切片周期, 默认是5ms(如果一个task运行超过该周期, 下一个task执行之前, 会把控制权归还浏览器)
let yieldInterval = 5;

let deadline = 0;
const maxYieldInterval = 300;
let needsPaint = false;
const scheduling = navigator.scheduling;
// 是否让出主线程
shouldYieldToHost = function() {
  const currentTime = getCurrentTime();
  if (currentTime >= deadline) {
    if (needsPaint || scheduling.isInputPending()) {
      // There is either a pending paint or a pending input.
      return true;
    }
    // There's no pending input. Only yield if we've reached the max
    // yield interval.
    return currentTime >= maxYieldInterval; // 在持续运行的react应用中, currentTime肯定大于300ms, 这个判断只在初始化过程中才有可能返回false
  } else {
    // There's still time left in the frame.
    return false;
  }
};

// 请求绘制
requestPaint = function() {
  needsPaint = true;
};

// 设置时间切片的周期
forceFrameRate = function(fps) {
  if (fps < 0 || fps > 125) {
    // Using console['error'] to evade Babel and ESLint
    console['error'](
      'forceFrameRate takes a positive int between 0 and 125, ' +
        'forcing frame rates higher than 125 fps is not supported',
    );
    return;
  }
  if (fps > 0) {
    yieldInterval = Math.floor(1000 / fps);
  } else {
    // reset the framerate
    yieldInterval = 5;
  }
};

四、核心总结

本节主要分析了react调度器核心源码实现及流程, 介绍了时间切片可中断渲染等特性在任务调度循环中的实现,react实现异步可中断渲染成为现实。

  • 异步:宏任务Messagechannnel实现,每个任务都是异步执行的。

  • 可中断:时间分片实现,一个时间分片内可能执行多个任务或者一个长任务区别,所以任务间和任务内,都要判断是否到期,每个任务内部按照fiber单元执行,判断时间片是否到期。

相关推荐
aklry6 小时前
elpis之学习总结
前端·vue.js
_advance6 小时前
我是怎么把 JavaScript 的 this 和箭头函数彻底搞明白的——个人学习心得
前端
右子6 小时前
React 编程的优雅艺术:从设计到实现
前端·react.js·mobx
清灵xmf7 小时前
npm install --legacy-peer-deps:它到底做了什么,什么时候该用?
前端·npm·node.js
超级大只老咪7 小时前
字段行居中(HTML基础语法)
前端·css·html
IT_陈寒7 小时前
Python开发者必看!10个高效数据处理技巧让你的Pandas代码提速300%
前端·人工智能·后端
只_只8 小时前
npm install sqlite3时报错解决
前端·npm·node.js
FuckPatience8 小时前
Vue ASP.Net Core WebApi 前后端传参
前端·javascript·vue.js
南北是北北8 小时前
类型推断、重载与桥方法
面试