React源码解析:Scheduler(调度器)

685 阅读10分钟

前言

React 16 后引入了 Scheduler(调度器)的概念,它是一个优先级队列,会根据任务的优先级,调度任务队列,取出优先级最高的任务去执行。当任务的时间超 5ms 后,Scheduler 会中断没完成的任务,交出 JS 线程的控制器给浏览器,避免界面交互卡顿。然后在下一次同样根据优先级,执行新任务或恢复上次未执行完的任务。

那么 Scheduler 具体是如何工作的呢?

任务队列与调度者创建

Scheduler 里面有两个优先级任务队列:

  • taskQueue:存放正常的任务
  • timerQueue:存放延时任务

taskQueue、timerQueue 都是最小堆的数据结构

当 Scheduler 开始调度时,会先从 taskQueue 中,根据优先级,拿出对应的任务执行。执行完毕后,会将这个任务从 taskQueue 中删除。当 taskQueue 中的任务全部完成后,会去 timerQueue 中看有没有过期的任务,有的话就放到 taskQueue 中 执行。

相关代码如下:

function unstable_scheduleCallback(priorityLevel, callback, options) {
  /**
      第一部分
  **/
  var currentTime = getCurrentTime(); //当前时间
  var startTime; // 开始时间
  // 根据是否存在 delay,计算开始时间
  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 = IMMEDIATE_PRIORITY_TIMEOUT;
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  }

  // 任务过期时间:开始时间 + 延时时间
  var expirationTime = startTime + timeout;

  /**
      第二部分
  **/
  // 创建新任务
  var newTask = {
    id: taskIdCounter++,
    callback, // callback = performConcurrentWorkOnRoot
    priorityLevel,
    startTime,
    expirationTime,
    sortIndex: -1,
  };
  if (enableProfiling) {
    newTask.isQueued = false;
  }
  //开始时间大于当前时间,表示当前任务是一个延时任务
  if (startTime > currentTime) {
    // 以开始时间作为优先级排序
    newTask.sortIndex = startTime;
    // 将这个延时任务加入到 timerQueue 中
    push(timerQueue, newTask);

    //当 taskQueue 中执行完所有的任务,且 timerQueue 有延时任务
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 创建一个timeout 作为调度者,回调函数是 handleTimeout
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    // 否则,是个正常任务,将 expireationTime 作为优先级排序字段
    newTask.sortIndex = expirationTime;
    //加入 taskQueue 中
    push(taskQueue, newTask);
    if (enableProfiling) {
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    }
    //判断 Scheduled 是否正在调度任务
    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      //没有的话则创建一个调度者,有的话则直接使用上一个调度者,然后开始调度任务
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

上面的代码的流程是:

  • 根据是否存在 delay,计算 startTime 开始时间,根据任务优先级,计算 expirationTime 过期时间
  • 根据条件 startTime > currentTime,判断是正常任务(false)还是延时任务(true)
    • 若是延时任务,则以 startTime 作为优先级排序字段,加入 timerQueue,然后判断 taskQueue 中任务是否都已经完成,如果都已经完成,则调用 requestHostTimeout(handleTimeout, startTime - currentTime) 创建一个 timeout 作为调度者。
    • 若是正常任务,则以 expirationTime 作为优先级排序字段,加入 taskQueue,判断当前有没有调度者,如果没有,则调用 requestHostCallback(flushWork) 创建调度者,否则使用上一次的调度者,然后调度任务。

清楚了上面的过程后,我们继续看一下后续流程是什么。

当任务是延时任务时,会进入 handleTimeout 回调,而这个函数的代码如下:

function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;

  //检查 timerQueue 中是否有已过期的任务
  //有的话则将过期任务添加到 taskQueue 进行执行
  advanceTimers(currentTime);

  //isHostCallbackScheduled:是否有正在执行的调度,如果没有,则会创建一个调度者,去执行任务
  if (!isHostCallbackScheduled) {
    // 如果 taskQueue 里面还有任务没执行完,走 requestHostCallback,创建调度者
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    } else {
      // 否则,把 timerQueue 中的任务拿出来,如果存在延时任务
      // 则通过 requestHostTimeout 创建 timeout 作为调度者
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

handleTimeout 做的事情是:

  • 通过 advanceTimers 函数,判断 timerQueue 里面有没有过期的任务,如果有,放入 taskQueue
  • 判断当前是不是有调度者正在调度任务,如果没有,会去创建调度者,分两种情况
    • 如果此时 taskQueue 还有任务没执行完,则调用 requstHostCallback 创建调度者
    • 否则,如果 timerQueue 中有延时任务没执行,则调用 requestHostTimeout 创建一个 timeout 作为调度者

那么 requestHostCallback 里面有干了什么?

// 注意:传入的 callback 就是前文代码里面的 flushWork
function requestHostCallback(callback) {
  // 记录 flushWork
  scheduledHostCallback = callback;
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    //调用这个方法
    schedulePerformWorkUntilDeadline();
  }
}


// 与 schedulePerformWorkUntilDeadline 相关的代码
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;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  schedulePerformWorkUntilDeadline = () => {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

上面的代码流程是:

  • scheduledHostCallback 变量 记录传入的 callbak,也就是 flushWork
  • 根据不同的执行环境,分三种情况创建调度者:
    1. 如果是老版本 IE,则使用 setImmediate 作为调度者
    2. 如果是默认浏览器环境,则使用 MessageChannel 作为调度者
    3. 如果前两种情况都不能实现,则使用 setTimeout 作为调度者

那么我们现在在这里总结下前面的流程:

  1. 确认任务类型:根据 currentTime 当前时间、是否存在 delay、任务优先级,计算 startTime、expirationTime 用于优先级排序的字段,然后判断是 newTask 正常任务还是延时任务,然后加入到 taskQueue 或 timerQueue 中。
  2. 创建调度者:延时任务创建 timeout 作为创建者,正常任务根据当前执行环境,可以创建 setImmediate、MessageCahnnel、setTimeout 三种类型的调度者。

到这里,任务队列分别存储了对应类型的任务,并以相关规则优先级排序,调度者也已经被创建。那么接下来看如何调度任务的。

执行任务调度

上面创建调度者的代码中,三种类型最终都会调用 performWorkUntilDeadline 函数,该函数就是执行任务的函数。

const performWorkUntilDeadline = () => {
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    
    try {
      // 调用 scheduledHostCallback 函数
      // 这个函数其实就是 requestHostCallback(flushWork) 中的 flushWork
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      // hashMore 表示是否还有任务需要执行
      // 如果为 true 表示有任务在执行中被中断,需要重新执行,那么则需要重新发起一个调度
      if (hasMoreWork) {
        // 重新发起一个调度,也就是那三种情况下创建调度者
        schedulePerformWorkUntilDeadline();
      } else {
        // 否则就表示 taskQueue 中的任务都执行完成了
        // 需要将调度者释放,为下一次调度做准备
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};

performWorkUntilDeadline 中,会先调用 scheduledHostCallback 函数,而这个函数其实就是之前 requestHostCallback(flushWork) 中的 flushWork

function flushWork(hasTimeRemaining, initialTime) {
 ...
 return workLoop(hasTimeRemaining, initialTime);
 ...
}

flushWork 内部调用 workLoop 函数,将 workLoop 的返回值返回出去,赋值给了 hasMoreWork 变量

也就是说,真正执行任务的地方是在 workLoop 中

function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  
  //检查 timerQueue 中是否有过期的任务,有就添加到 taskQueue 中执行
  advanceTimers(currentTime);

  //取出优先级最高的任务
  currentTask = peek(taskQueue);

  // work循环
  while (
    currentTask !== null &&
    !(enableSchedulerDebugging && isSchedulerPaused)
  ) {
    //当前任务的过期时间是否大于当前时间,大于则表示没有过期则不需要立即执行
    //hasTimeRemaining: 表示是否还有剩余时间,剩余时间不足则需要中断当前任务,让其他任务先执行
    //shouldYieldToHost: 是否应该中断当前任务
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      break;
    }

    //判断当前任务的回调函数 callback 是否为空,为空则会将当前任务从任务队列中删除
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      //回调函数设置为null,表示任务执行完成后会从 taskQueue 中删除
      currentTask.callback = null;
      //获取任务的优先级
      currentPriorityLevel = currentTask.priorityLevel;
      //判断当前任务是否过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      //执行任务,记录执行完成后的结果
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        //任务执行完成后的结果返回的是一个函数表示当前任务没有完成
        //则将这个函数作为当前任务新的回调函数,在下一次While循环时调用
        currentTask.callback = continuationCallback;
      } else {
        //否则,删除当前任务
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      //检查 timerQueue 中是否有过期的任务,有就加入 taskQueue 中
      advanceTimers(currentTime);
    } else {
      //删除当前任务
      pop(taskQueue);
    }
    //从taskQueue中继续获取任务,如果上一次任务没有完成,那么不会从taskQueue中删除,获取的还是上一次的任务
    //接下来会继续执行它
    currentTask = peek(taskQueue);
  }

  //当前任务被中断,currentTask则不会为null,则会返回true,
  //scheduler会继续发起调度,执行任务
  if (currentTask !== null) {
    return true;
  } else {
    //currentTask为 null,则表示 taskQueue 中的任务都执行完成了
    //则判断 timerQueue 中是否有过期任务
    //有的话则添加到 taskQueue 中,并重新发起调度执行任务
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

workLoop 中的流程是:

  • 判断是否终止当前任务,判断的条件是:

    • currentTask.expirationTime > currentTime:当前任务的过期时间是否大于当前时间,如果大于,则表示还没过期,让权给其他已经过期的任务
    • !hasTimeRemaining:是否还有剩余时间,如果没有,则需要中断当前任务
    • shouldYieldToHost():这个函数其实会先判断当前任务的执行时间是否小于 5ms,如果是,返回 false 不需要中断,如果超过了,则返回 true 中断
  • 如果不中断当前任务,则执行当前任务的执行函数:

    • currentTask.callback 是否是 function
      • 如果不是,从 taskQueue 中删除该任务
      • 否则执行 callback,将执行的结果记录给 continuationCallback 变量,continuationCallback 如果是 null,表示当前任务执行完毕,将它从 taskQueue 中删除。否则没执行完,就让权给其他优先级更高的任务。

最后,拿到结果,回到 performWorkUntilDeadline 函数中:

else {
   // 否则就表示 taskQueue 中的任务都执行完成了
   // 需要将调度者释放,为下一次调度做准备
   isMessageLoopRunning = false;
   scheduledHostCallback = null;
}

走到这里的时候 Scheduler 的调度就结束了。

总结

任务队列管理:

每个React任务都有开始时间(StartTime)和任务到期时间(expirationTime),Scheduler 使用两个优先队列存储任务 TaskQueueTimerQueue 存放任务,前者存放即将执行的任务,后者则存放延时执行任务

  • TaskQueue 是以 expirationTime 作为优先级排序字段,到期时间小的排在前面。taskQueue 中的任务会不断执行
  • TimerQueue 中任务是以 startTime = currentTime + delay 作为优先级排序字段,在 timerQueue 中的任务会采用 setTimeout 定时器,等到任务等待时间过后再放到 taskQueue 中 **

创建调度者:

React 会根据当前执行环境,创建不同的调度者,即:setImmediate、MessageChannel、setTimeout 三种类型

  • 老版本IE下,使用 setImmediate 作为调度者
  • 浏览器默认使用 MessageChannel 作为调度者
  • 当上面都无法满足时,使用 setTimeout 作为调度者

执行任务调度时:

会首先判断当前任务 currentTask 是否会被中断

  • 如果任务被中断,会去 taskQueue 中拿任务,如果 taskQueue 中没有任务,去 timerQueue 中拿任务,然后继续执行任务调度

  • 如果任务没有被中断,会判断当前执行的任务的执行回调函数 currentTask.callback 是否为空

    • 如果为空则会将 currentTask 从 taskQueue 中删除

    • 如果不为空,则执行 currentTask.callback,将执行的结果记录到 continuationCallback 变量中,判断 continuationCallback 是否为空

      • 如果不为空,表示 currentTask 还没执行完,将 continuationCallback 作为 currentTask 下一次任务调度时的 callback,即:currentTask.callback = continuationCallback
      • 如果为空,表示 currentTask 执行完毕,然后从 taskQueue 中删除
  • 重复上面的步骤,不断消费 taskQueue 中的任务。当 taskQueue 为空,表示 React 所有任务执行完毕,最后会将调度者释放 scheduledHostCallback = null,为下一次调度做准备。

结语

以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论

相关文章

不用一行代码,搞懂React调度器原理

React任务调度器