React Scheduler 源码理解

802 阅读4分钟

基于react v17.3 scheduler0.20.1 功能:

  • 时间切片
  • 优先级调度

requestIdleCallbackrequestAnimationFrame 无法实现react团队的需求,所以自己实现了 Scheduler

调度任务 -- 创建一个宏任务

基于事件队列,创建宏任务不会卡死主线程,主线程将归还浏览器,执行相关渲染。

MessageChanel

相等于0s延迟的setTimeout,并且支持Web Worker

const channel = new MessageChannel();
const port = channel.port2;
// 通过onmessage来创建宏任务,执行performWorkUntilDeadline
// performWorkUntilDeadline功能:执行taskQueue中的所有task
channel.port1.onmessage = performWorkUntilDeadline;
// 函数功能:创建一个宏任务 执行performWorkUntilDeadline
schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};

setTimeout

不支持MessageChannel时备用。 在浏览器中,实际最小延迟为4s

schedulePerformWorkUntilDeadline = () => {
  // setTimeout 执行执行performWorkUntilDeadline
  localSetTimeout(performWorkUntilDeadline, 0);
};

每个宏任务给多少秒执行时间?

默认给 5s 执行时间 。 也可以会通过fps动态调整(forceFrameRatereact目前没启用)。 通过主线程运行了多长时间 减去 任务 开始时间, 提供判断是否需要让出主线程函数。

const frameYieldMs = 5; // 每帧5秒执行时间,也就是阻塞主线程5秒
let frameInterval = frameYieldMs;

// 是否要让出 主线程。  即判断当前分配的执行时间是否用完
function shouldYieldToHost() {
  // getCurrentTime: 是获取主线程已运行时间,(performance或者Date实现)
  // startTime: 每次performWorkUntilDeadline时 赋值getCurrentTime()
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false;
  }
  // InputPending 的优化 忽略
 	// ...

  return true;
}

优先级调度

此优先级 和 react中的lane不同。 react 中优先级调度任务,都会使用 unstable_scheduleCallback

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // 获取当前时间  (主线程已执行时间)  
  var currentTime = getCurrentTime();
	// 任务的 开始时间
  var startTime;
  // react中没有使用options。 即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:
      // -1
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      // 250
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      // maxSigned31BitInt  1073741823 V8中32位系统的最大整数大小。 
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      // 10000
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      // 5000
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }
	// 任务过期时间
  // 之后会判断,任务过期时间 小于 当前时间  就会被执行。
  var expirationTime = startTime + timeout;
  // 任务节点
  var newTask = {
    id: taskIdCounter++,
    callback,
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  
  // react中没有使用过options,自然也没用timerQueue
  if (startTime > currentTime) {
    // 延时任务 放入 timerQueue
    newTask.sortIndex = startTime;
    // 小顶堆排序,最早过期的排最前面
    push(timerQueue, newTask);
    // 若taskQueue中无任务,且newTask是timerQueu中将会最早过期的
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        // 若已有定时任务,则清除
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 调度一个定时器宏任务 handleTimeout
      // handleTimeout :
      //      1.会将timerQueue中的startTime小于currentTime的任务,加入taskQueue
      //      2.若当前无任务执行,taskQueue不为空则requestHostCallback(flushWork);
      //                       若taskQueeu为null,调度下一个timerQueue,若也为空则什么也不干
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 非延时任务,放入taskQueue
    newTask.sortIndex = expirationTime;
    // 小顶堆排序,最早过期的排最前面
    push(taskQueue, newTask);
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      // 若无清空taksQueu的调度函数执行,
      // 则通过开始调度 flushWork
      // 
      // flushWork : 会将分情况将所有 taks 执行。
      //             1. 已过期taks 执行
      //             2. 未过期taks,再次调度flushWork重复到1,2. 直到taksQueue清空
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

清空taskQueue -- performWorkUntilDeadline

// 简化后的函数,只留了核心代码
const performWorkUntilDeadline = () => {
  // scheduledHostCallback有taks时,会被赋值flushWork
  // 值不为空,说明有taks需要调度
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
		  // scheduledHostCallback有taks时,会被赋值flushWork
      // 就是执行flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // 若还有task(未过期任务)  则继续递调度
        //会一直调度到 taskQueue为空
        schedulePerformWorkUntilDeadline();
      } else {
        scheduledHostCallback = null;
      }
    }
  }
};
// flushWork 
// 简化后的函数,只留了核心代码
function flushWork(hasTimeRemaining, initialTime) {
  const previousPriorityLevel = currentPriorityLevel;
  try {
    // while执行 taskQueue
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    currentPriorityLevel = previousPriorityLevel;
  }
}
//workLoop 
//简化后的函数。  
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  advanceTimers(currentTime); // 检测timerQueue,将其中过期的timer 移到taskQueue
  currentTask = peek(taskQueue); // 拿出第一个过期taskQueue
  while ( currentTask !== null ) {
    if (
      // taks 按优先级设定的执行时间未到  
      currentTask.expirationTime > currentTime && shouldYieldToHost())
    ) {
      break;
    }
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 去除task中的callback,执行
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
      // 若返回值为函数,则taks更新callback,继续调度   未从taskQueue中推出
        currentTask.callback = continuationCallback;
      } else {
        if (currentTask === peek(taskQueue)) {
          // task完成 推出taskQueue
          pop(taskQueue);
        }
      }
      // 检测timerQueue,将其中过期的timer 移到taskQueue
      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }
    // 下一个task
    currentTask = peek(taskQueue);
  }
  if (currentTask !== null) {
    // 若还有task,则返回true,performWorkUntilDeadline中将会继续调度
    // taks未在上面执行,说明此taks的expirationTime未到
    return true;
  } else {
    // 没有task,则调度timer
    // react中暂时未启用timer,全部为task
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

总结

通过shouldYieldToHost 判断是否有执行时间,有则通过 unstable_scheduleCallback 进行优先级调度。 将需要调度的函数,创建task push到小顶堆(按时间排序)中。 只要有task,就进行轮询(创宏任务轮询)taskQueue 若过期,则立即执行。 未过期,则再次调度上述轮询,直到 taskQueue 清空。

通过宏任务轮询清除taskQueue 减少主线程阻塞。