源码分析之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的限制。

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

相关推荐
默默学前端25 分钟前
ES6模板语法与字符串处理详解
前端·ecmascript·es6
lxh011334 分钟前
记忆函数 II 题解
前端·javascript
我不吃饼干41 分钟前
TypeScript 类型体操练习笔记(三)
前端·typescript
华仔啊44 分钟前
除了防抖和节流,还有哪些 JS 性能优化手段?
前端·javascript·vue.js
CHU7290351 小时前
随时随地学新知——线上网课教学小程序前端功能详解
前端·小程序
清粥油条可乐炸鸡1 小时前
motion入门教程
前端·css·react.js
这是个栗子1 小时前
【Vue3项目】电商前台项目(四)
前端·vue.js·pinia·表单校验·面包屑导航
前端Hardy1 小时前
Electrobun 正式登场:仅 12MB,JS 桌面开发迎来轻量化新方案!
前端·javascript·electron
树上有只程序猿1 小时前
新世界的入场券,不再只发给程序员
前端·人工智能
confiself1 小时前
deer-flow前端分析
前端