scheduler是react在浏览器负责调度整个应用独立的包,其中包含两个功能:

  1. 时间切片
  2. 优先级调度

    时间切片

    js是单线程的,在浏览器中不同时刻执行的任务不同,比如宏任务和微任务

切片的原理就是在浏览器可以执行js的时刻去执行一些js任务, 把一个任务分成多个工作单元,在浏览器有空闲时机执行就是时间切片的意义

切片的工作通过task宏任务来做确定在空闲时机被执行,宏任务有setTimeout 还有一个新的API就是MessageChannel 执行比之更靠前

时间切片的本质是模拟的requestIdlCallback 的执行,因为这个API是在浏览器重排重绘以后如果有空余时间才会被调用,在时间循环的尾部,属于低优先级API不会阻塞页面的渲染

调度任务执行时是通过宏任务实现的。宏任务中最常见的就是setTimeout, 但是这个task是在下一次时间环是才会执行。但是现在存在一个比这个task执行时机更靠前,那就是MessageChannel

优先级调度

scheduler的优先级和react的优先级不同,模块会对外暴漏一个方法提供使用
也就是runWithPriority:

  1. function unstable_runWithPriority(priorityLevel, eventHandler) {
  2. switch (priorityLevel) {
  3. case ImmediatePriority:
  4. case UserBlockingPriority:
  5. case NormalPriority:
  6. case LowPriority:
  7. case IdlePriority:
  8. break;
  9. default:
  10. priorityLevel = NormalPriority;
  11. }
  12. var previousPriorityLevel = currentPriorityLevel;
  13. currentPriorityLevel = priorityLevel;
  14. try {
  15. return eventHandler();
  16. } finally {
  17. currentPriorityLevel = previousPriorityLevel;
  18. }
  19. }

currentPriorityLevel 这个变量在执行一个回调时会被修改为当前的优先级。回调函数内部会通过获取当前的Scheduler模块的优先级会返被返回这个变量

在scheduler中的优先级,决定了该任务的执行时机,一时间出现的多个任务会存放在两个队列里,是由小顶堆构成的优先队列,排序标准就是此任务的优先级。

优先级的本质就是过期时间。 不同的优先级以为这不同的任务过期时间

  1. // 优先级的过期时间
  2. // 立即执行优先级, 是过期任务立即同步执行
  3. var IMMEDIATE_PRIORITY_TIMEOUT = -1;
  4. // 用户与页面交互的优先级, 常见的由focus、click等,
  5. var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
  6. // 普通优先级
  7. var NORMAL_PRIORITY_TIMEOUT = 5000;
  8. // 低优先级
  9. var LOW_PRIORITY_TIMEOUT = 10000;
  10. // 空闲优先级
  11. var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
  12. var maxSigned31BitInt = 1073741823;

不同的优先级分出不同的任务,比如高优先级的任务是立即执行的任务, 低优先级的任务是等待执行的任务,可以分出两个队列

  • timerQueue: 保存的未过期的任务,等待被执行的任务
  • taskQueue: 保存已经要执行的任务, 立即被执行的任务

每次在执行时需要将taskQueue中的任务取出并执行。也就是:

  1. function unstable_scheduleCallback(priorityLevel, callback, options) {
  2. var currentTime = getCurrentTime();
  3. var startTime;
  4. if (typeof options === 'object' && options !== null) {
  5. var delay = options.delay;
  6. if (typeof delay === 'number' && delay > 0) {
  7. // 任务被延迟, 当前的时间在加上一个时间为任务重新设置一个超时时间
  8. startTime = currentTime + delay;
  9. } else {
  10. startTime = currentTime;
  11. }
  12. } else {
  13. startTime = currentTime;
  14. }
  15. // 根据优先级得出任务的过期时间
  16. var timeout;
  17. switch (priorityLevel) {
  18. case ImmediatePriority:
  19. timeout = IMMEDIATE_PRIORITY_TIMEOUT;
  20. break;
  21. case UserBlockingPriority:
  22. timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
  23. break;
  24. case IdlePriority:
  25. timeout = IDLE_PRIORITY_TIMEOUT;
  26. break;
  27. case LowPriority:
  28. timeout = LOW_PRIORITY_TIMEOUT;
  29. break;
  30. case NormalPriority:
  31. default:
  32. timeout = NORMAL_PRIORITY_TIMEOUT;
  33. break;
  34. }
  35. var expirationTime = startTime + timeout;
  36. var newTask = {
  37. id: taskIdCounter++,
  38. callback,
  39. priorityLevel,
  40. startTime,
  41. expirationTime,
  42. sortIndex: -1,
  43. };
  44. if (enableProfiling) {
  45. newTask.isQueued = false;
  46. }
  47. if (startTime > currentTime) {
  48. // 新任务入队, 如果未过期就保存到timerQueue
  49. newTask.sortIndex = startTime;
  50. push(timerQueue, newTask); // 入队
  51. if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
  52. // 因为小顶堆新加入了一个任务,顺序重新调整, 就取消上次的timeout回调, 重新开始等待超时任务
  53. if (isHostTimeoutScheduled) {
  54. // 取消上次的timeout回调
  55. cancelHostTimeout();
  56. } else {
  57. isHostTimeoutScheduled = true;
  58. }
  59. // 如果目前正在进行的任务为空,并且等待任务就是最新加入的任务
  60. // 就加入一个timeout回调,等待过期之后放入taskQueue执行
  61. requestHostTimeout(handleTimeout, startTime - currentTime);
  62. }
  63. } else {
  64. newTask.sortIndex = expirationTime;
  65. push(taskQueue, newTask);
  66. if (enableProfiling) {
  67. markTaskStart(newTask, currentTime);
  68. newTask.isQueued = true;
  69. }
  70. // 如果此次任务为立即执行的优先级,就执行flushWork立即开始执行这个任务
  71. if (!isHostCallbackScheduled && !isPerformingWork) {
  72. isHostCallbackScheduled = true;
  73. requestHostCallback(flushWork);
  74. }
  75. }
  76. return newTask;
  77. }
  78. // 这个方法会在执行会在每次执行时都重置以下两个队列
  79. // 在第一个加入队列未过期的任务过期时会重新把过期的任务拿到taskQueue中执行
  80. // 调用出重新执行flushWork
  81. function advanceTimers(currentTime) {
  82. let timer = peek(timerQueue);
  83. while (timer !== null) {
  84. if (timer.callback === null) {
  85. pop(timerQueue);
  86. } else if (timer.startTime <= currentTime) {
  87. pop(timerQueue);
  88. timer.sortIndex = timer.expirationTime;
  89. push(taskQueue, timer);
  90. if (enableProfiling) {
  91. markTaskStart(timer, currentTime);
  92. timer.isQueued = true;
  93. }
  94. } else {
  95. // Remaining timers are pending.
  96. return;
  97. }
  98. timer = peek(timerQueue);
  99. }
  100. }
  101. // 在flushWork开始时,会取消掉等待中的任务,因为进入此方法后的任务都是立即执行的
  102. // 函数会调用workLoop,执行完成以后会再次把等待中的任务取出
  103. isHostCallbackScheduled = false;
  104. if (isHostTimeoutScheduled) {
  105. // 取消timeout回调,也就是等待被执行的任务
  106. isHostTimeoutScheduled = false;
  107. cancelHostTimeout();
  108. }

每次注册需要调度的任务都是一个优先级加上一个回调(回调函数内做其他事情,比如commit,交给Scheduler包后,会根据优先级排序,先执行taskQueue 中的高优先级任务,在tmerQueue中最快过期的任务过期时重新拿到taskQueue中执行

在从去taskQueue中取出时有一步关键的操作:

  1. // 当前执行的任务
  2. const continuationCallback = callback(didUserCallbackTimeout);
  3. currentTime = getCurrentTime();
  4. // 执行以后返回值如果时要给函数,会被重新赋值给callback属性。
  5. // 意思是这个任务没有执行完毕,需要先等等
  6. if (typeof continuationCallback === 'function') {
  7. currentTask.callback = continuationCallback;
  8. markTaskYield(currentTask, currentTime);
  9. } else {
  10. if (enableProfiling) {
  11. markTaskCompleted(currentTask, currentTime);
  12. currentTask.isQueued = false;
  13. }
  14. if (currentTask === peek(taskQueue)) {
  15. pop(taskQueue);
  16. }
  17. }
  18. advanceTimers(currentTime);
  19. }

如果返回值不是函数则当前任务从队列中清除
在render阶段被调度时有这样一句:

  1. if (root.callbackNode === originalCallbackNode) {
  2. // 任务没有执行完毕。 originalCallbackNode 就是当前的任务
  3. // 假如当前的任务没有被其他打断,正常在执行,那么二者相同
  4. // 这里返回的回调就会被重新赋值为callabck
  5. return performConcurrentWorkOnRoot.bind(null, root);
  6. }

Scheduler 包是独立于react的,这个包的优先级是scheduler包内部使用的优先级,和外部的优先级不是一个概念。包在被react使用时会有两个函数, 一个是用于将scheduler的优先级转换为react的优先级,另一个反过来。

Scheduler对外保露了一个方法unstable_runWithPriority 方法接受一个优先级和一个回调函数,执行回调时在内部获取的优先级的方法都会取到执行runWithPriority方法给到的优先级



function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  currentPriorityLevel = priorityLevel;

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
  }
}


// 函数执行时用于获取当前的优先级
function unstable_getCurrentPriorityLevel() {
  return currentPriorityLevel;
}

scheduleCallback 是用于以一个优先级注册一个回调函数,这个函数利用浏览器空闲时间去执行里面的任务。
runWithPriority 是以一个优先级执行一个回调函数,比如事件派发时