源码分析之React中Scheduler调度器的任务优先级

概览

React应用中的更新(如useEffectsetState等)都会被转化为一个任务task,并将其分配给Scheduler进行调度。每个任务都有一个优先级priority,而Scheduler会根据优先级来决定任务的执行顺序。

优先级

优先级定义

js 复制代码
// React 中的优先级定义(数值越小,优先级越高)
const NoPriority = 0;           // 无优先级
const ImmediatePriority = 1;    // 立即执行优先级
const UserBlockingPriority = 2; // 用户阻塞优先级
const NormalPriority = 3;      // 正常优先级
const LowPriority = 4;         // 低优先级
const IdlePriority = 5;        // 空闲优先级
优先级具体含义
  1. ImmediatePriority
  • 用途:需要立即同步执行的任务
  • 场景:
    • 离散的用户输入(如点击、按键)
    • 同步的React渲染
    • 在并发模式Concurrent Mode中,某些需要立即执行的副作用
  • 特点:不能被中断,必须立即完成
  1. UserBlockingPriority
  • 用途:用户交互相关的任务
  • 场景:
    • 连续的用户输入(如拖拽、滚动)
    • 动画更新
    • 用户触发的状态更新
  • 特点:需要在下一帧前完成,确保流畅的用户体验
  1. NormalPriority
  • 用途:默认的任务优先级
  • 场景:
    • 大部分状态更新
    • 网络请求完成后的渲染
    • 普通的副作用
  • 特点:可以被更高优先级的任务打断
  1. LowPriority
  • 用途:可以延迟执行的任务
  • 场景:
    • 数据预加载
    • 非关键的渲染
    • 分析日志
  • 特点:在浏览器空闲时执行
  1. IdlePriority
  • 用途:在浏览器完全空闲时执行的任务
  • 场景:
    • 在非必要的后台任务
    • 性能监控
    • 离线数据同步
  • 特点:只在浏览器空闲时执行,可能永远不会执行。

优先级作用

Scheduler调度器通过两个队列来管理调度任务:taskQueue任务队列和timerQueue定时器队列,以下是它们的区别:

  1. taskQueue任务队列
  • 存储可立即执行的任务
  • expirationTime排序
  • 存储当前需要执行的任务
  1. timerQueue定时器队列
  • 存储延迟执行的任务
  • startTime排序
  • 用于管理未来要执行的任务

这两个队列都是按照最小堆排序,而优先级就是排序的关键,优先级最小的在队列的最前面。

任务的注册unstable_scheduleCallback

unstable_scheduleCallback是Scheduler调度器提供给Reconciler协调器的方法,用于创建任务,并将其放入队列中进行调度执行。其源码实现如下:

js 复制代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前时间
  var currentTime = getCurrentTime();
  var startTime;
  // 判断参数options是否有值,其delay值为延迟执行的时间,若设置了delay,则startTime为currentTime和delay之和;反之,startTime就是currentTime,表示需要立即执行
  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:
      timeout = -1;
      break;
    case UserBlockingPriority:
      timeout = 250;
      break;
    case IdlePriority:
      timeout = Math.pow(2, 30) - 1;
      break;
    case LowPriority:
      timeout = 10000;
      break;
    case NormalPriority:
    default:
      timeout = 5000;
      break;
  }

  // 计算过期时间
  var expirationTime = startTime + timeout;

  // 创建任务对象
  var newTask = {
    id: taskIdCounter++, // 唯一标识
    callback, // 实际要执行的任务
    priorityLevel, // 原始优先级
    startTime, // 何时可以开始执行
    expirationTime, // 何时必须执行(过期必须执行)
    sortIndex: -1, // 在队列中的排序依据,初始值为-1
  };
 
  // 延时任务
  if (startTime > currentTime) {
    // 延时任务按照开始时间排序
    newTask.sortIndex = startTime;
    // 将任务插入到timerQueue队列中,push方法会使得timerQueue队列重排序,sortIndex最小的任务优先级最高,会在队列最前面
    push(timerQueue, newTask);
    // 若任务队列为空,且新任务是定时器队列最早的任务,则满足条件
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // 若 设置过定时器,则取消定时器
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        // 修改定时器标志
        isHostTimeoutScheduled = true;
      }
      // 调用requestTimeout方法,创建定时器
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 若没有设置延迟时间delay,则将任务的过期时间设为任务的sortIndex
    newTask.sortIndex = expirationTime;
    // 将新任务放到taskQueue任务队列中
    push(taskQueue, newTask);
    // 若没有正在进行的调度且没有正在执行的工作,则调用requestHostCallback
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    }
  }
  // 返回任务对象
  return newTask;
}

延时任务队列

requestHostTimeout

requestHostTimeout方法会创建一个定时器,在ms后,调用callback方法。在注册延时任务时,满足条件后,会在delay毫秒后调用的callback回调就是handleTimeout方法。

js 复制代码
function requestHostTimeout(callback, ms) {
  taskTimeoutID = localSetTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}
handleTimeout

handleTimeout方法在延迟时间到了后就会触发。

js 复制代码
function handleTimeout(currentTime) {
  // 将定时器标志置为false,方便后续创建定时器
  isHostTimeoutScheduled = false;

  // 处理定时器队列
  advanceTimers(currentTime);

  // 若此时没有任务执行
  if (!isHostCallbackScheduled) {
    // 任务队列不为空
    if (peek(taskQueue) !== null) {
      // 修改任务执行标志
      isHostCallbackScheduled = true;
      // 调用requestHostCallback调度任务
      requestHostCallback();
    } else {
    // 若任务队列为空,则从定时器队列中取出优先级最高的定时器任务  
      const firstTimer = peek(timerQueue);
      // 若定时器任务存在,则调用requestHostTimeout创建新的定时器 
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}
advanceTimers

advanceTimers方法用于处理定时器队列和任务队列,具体来讲就是遍历定时器队列,将定时器队列中过期的任务按照优先级放在任务队列中。

js 复制代码
function advanceTimers(currentTime) {
  // 取出定时器队列中优先级最高的任务
  let timer = peek(timerQueue);
  while (timer != null) {
    // 若定时器任务的回调为null,则调用pop方法,移除该任务
    if (timer.callback === null) {
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // 若该任务的开始时间小于或等于当前时间,则说明该任务已过期,也需要移除该任务
      pop(timerQueue);
      // 修改任务的sortIndex为expirationTime过期时间
      timer.sortIndex = timer.expirationTime;
      // 将该任务放到任务队列中
      push(taskQueue, timer);
    } else {
      // 其他情况,跳出循环
      return;
    }
    // 从定时器队列中,取下一个优先级次高的任务
    timer = peek(timerQueue);
  }
}

任务队列

从上面的分析可以看出,延时任务队列的任务最终也会放入到任务队列中去进行调度执行。任务队列的调度会调用requestHostCallback方法。

requestHostCallback
js 复制代码
function requestHostCallback() {
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

requestHostCallback的内部就是根据标志isMessageLoopRunning调用schedulePerformWorkUntilDeadline方法,而该方法设计的很巧妙。

schedulePerformWorkUntilDeadline
js 复制代码
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout:null;

const localSetTimeout = typeof setImmediate !=='undefined'? setImmediate:null; 

let schedulePerformWorkUntilDeadline;
if(typeof localSetImmediate === 'function'){
  schedulePerformWorkUntilDeadline = ()=>{
    localSetImmediate(performWorkUntilDeadline);
  }
}else if(typeof MessageChannel !== 'undefined'){
   const channel = new MessageChannel();
   const port = channel.port2;
   channel.port1.onmessage = performWorkUntilDeadline;  
   schedulePerformWorkUnitDeadline=()=>{
    port.postMessage(null)
   } 
}else{
  schedulePerformWorkUntilDeadline = ()=>{
    localSetTimeout(performWorkUntilDeadline,0)
  }
}

Scheduler调度优先使用setImmediate,因为它不会阻止Node.js进程退出,其次考虑使用MessageChannel消息通道,它的执行时机是在当前任务完成后、渲染之前立即执行回调,且没有4ms的最小延迟,兜底方案是setTimeout定时器,该定时器在前者之后才执行,而且会有4ms的限制。

通过上述逐渐降级的兼容方案,实现了任务的异步调度。

相关推荐
波波0072 小时前
每日一题:在 .NET 中遍历集合(如 List<T>、数组、字典)的过程中进行增删改查会不会有影响?可能引发哪些问题?实际开发中应如何避免?
前端·list
念念不忘 必有回响2 小时前
码云流水线前端资源传输至目标服务器
运维·服务器·前端
我是伪码农2 小时前
Vue 2.2
前端·javascript·vue.js
●VON2 小时前
React Native for OpenHarmony:深入剖析 Switch 组件的状态绑定、无障碍与样式定制
javascript·学习·react native·react.js·von
时光追逐者2 小时前
一个基于 .NET + Vue 实现的通用权限管理平台(RBAC模式),前后端分离模式,开箱即用!
前端·vue.js·c#·.net·.net core
Aotman_2 小时前
Vue el-table 表尾合计行
前端·javascript·vue.js·elementui·前端框架·ecmascript
摘星编程2 小时前
React Native鸿蒙:ProgressBar圆角进度条
react native·react.js·harmonyos
静小谢3 小时前
vue3实现语言切换vue-i18n
前端·javascript·vue.js
东东5163 小时前
资产管理信息系统ssm+vue
前端·javascript·vue.js