【React 源码阅读】Scheduler

1 背景

React 在 18 版本引入了 Concurrent 模式,而这个模式则是用 Scheduler 这个包实现的。
在这篇文章里,我们来看下它的实现原理是什么。

2 前置知识

在正式阅读源码之前,我们还是有一些前置的知识需要了解的,分别是:

  • 小顶堆:Scheduler 内用来进行优先级排序的数据结构
  • 浏览器事件循环机制:Scheduler 实现的底层原理

2.1 小顶堆

堆是一棵完全二叉树,即除了最后一层外,所有层都完全填满,且最后一层的节点尽可能靠左排列。
最小堆:每个节点的值都小于或等于其子节点的值(父节点 ≤ 子节点)。

graph TD
    A(( 1 )) --> B(( 2 ))
    A --> C(( 3 ))
    B --> D(( 4 ))
    B --> E(( 5 ))
    C --> F(( 6 ))
    C --> G(( 7 ))
    D --> H(( 8 ))
    D --> I(( 9 ))

通常用数组实现,假设节点索引为 i:

  • 左子节点:2i+1
  • 右子节点:2i+2
  • 父节点:(i - 1)/2

2.1.2 建堆

  1. 节点 push 到数组末尾
  2. 比较节点与父节点大小,如果比父节点小,那么节点往上浮直至到达堆顶,否则固定在后面的位置

2.1.3 出堆

  1. 将堆顶的节点替换为堆底的节点
  2. 堆顶节点跟左节点比较大小,若堆顶节点大则交换直至到达堆底
  3. 否则堆顶节点跟右节点比较大小,若堆顶节点大则交换直至到达堆底
  4. 若以上两个条件都不满足,那么保持目标节点在原位置不变

2.1.4 完整代码

type Heap = Array;
type Node = {
  id: number,
  sortIndex: number,
  ...
};

export function push(heap: Heap, node: T): void {
  const index = heap.length;
  heap.push(node);
  siftUp(heap, node, index);
}

export function peek(heap: Heap): T | null {
  return heap.length === 0 ? null : heap[0];
}

export function pop(heap: Heap): T | null {
  if (heap.length === 0) {
    return null;
  }
  const first = heap[0];
  const last = heap.pop();
  if (last !== first) {
    // $FlowFixMe[incompatible-type]
    heap[0] = last;
    // $FlowFixMe[incompatible-call]
    siftDown(heap, last, 0);
  }
  return first;
}

function siftUp(heap: Heap, node: T, i: number): void {
  let index = i;
  while (index > 0) {
    const parentIndex = (index - 1) >>> 1;
    const parent = heap[parentIndex];
    if (compare(parent, node) > 0) {
      // The parent is larger. Swap positions.
      heap[parentIndex] = node;
      heap[index] = parent;
      index = parentIndex;
    } else {
      // The parent is smaller. Exit.
      return;
    }
  }
}

function siftDown(heap: Heap, node: T, i: number): void {
  let index = i;
  const length = heap.length;
  const halfLength = length >>> 1;
  while (index < halfLength) {
    const leftIndex = (index + 1) * 2 - 1;
    const left = heap[leftIndex];
    const rightIndex = leftIndex + 1;
    const right = heap[rightIndex];

    // If the left or right node is smaller, swap with the smaller of those.
    if (compare(left, node) < 0) {
      if (rightIndex < length && compare(right, left) < 0) {
        heap[index] = right;
        heap[rightIndex] = node;
        index = rightIndex;
      } else {
        heap[index] = left;
        heap[leftIndex] = node;
        index = leftIndex;
      }
    } else if (rightIndex < length && compare(right, node) < 0) {
      heap[index] = right;
      heap[rightIndex] = node;
      index = rightIndex;
    } else {
      // Neither child is smaller. Exit.
      return;
    }
  }
}

function compare(a: Node, b: Node) {
  // Compare sort index first, then task id.
  const diff = a.sortIndex - b.sortIndex;
  return diff !== 0 ? diff : a.id - b.id;
}

时间复杂度

  • 插入/删除最大(最小)值:O(log⁡n)
  • 建堆:O(n)
  • 堆排序:O(nlog⁡n)

2.1.5 总结

最后,我们对比一下其他排序的方式

数据结构 查找最小任务 插入任务 删除任务 适用性
小顶堆(React 使用) ✅ O(1) → 堆顶 ✅ O(log n) ✅ O(log n) ✅ 高效 + 支持动态优先级调度
数组(遍历找最小) ❌ O(n) ✅ O(1) ❌ O(n) ❌ 查找最小任务慢
链表(按时间排序) ✅ O(1)(头部是最小) ❌ O(n) 插入需遍历 ❌ O(n) 删除中间节点 ❌ 插入慢,重排复杂
队列(FIFO) ❌ 仅支持先进先出 ✅ O(1) ✅ O(1) ❌ 无法排序,完全不适合

2.2 浏览器事件循环机制

事件循环的流程:

  1. 执行宏任务
  2. 执行同步任务
  3. 遇到宏任务就放到宏任务队列,遇到微任务就放到微任务队列
  4. 如果执行完同步任务,就清空微任务队列
  5. 从宏任务队列拿出一个宏任务执行,继续以上步骤

用图来看的话更加清晰一些:

如果我们站在浏览器渲染的角度上来看的话,流程是这样的:

gantt
  dateFormat  HH:mm:ss
  axisFormat  %S

  section 宏任务(Macro Task)
  执行宏任务         :macro, 00:00:00, 2s

  section 微任务(Micro Task)
  清空微任务队列     :micro, after macro, 0.5s

  section requestAnimationFrame
  rAF 回调执行       :raf, after micro, 0.2s

  section 渲染(Repaint / Reflow)
  浏览器渲染         :render, after raf, 0.3s

  section requestIdleCallback
  执行 rIC 回调(如有空闲) :ric, after render, 0.8s

2.2.1 requestIdleCallback

requestIdleCallback 可以在浏览器空闲的时间里去执行,文档上对它的描述如下:

为什么 React 不直接使用 requestIdleCallback 来实现 Scheduler 呢?主要有以下几个问题:

  • 不确定性高:浏览器有可能根本没有空闲时间
  • 兼容性问题:有些浏览器根本不支持

2.2.2 requestAnimationFrame

从上面的图可以知道,requestAnimationFrame 是在浏览器渲染前执行的,那么 React 为什么不用它来实现呢?主要原因是:

  1. requestAnimationFrame + setTimeout 的方法不可控
  2. requestAnimationFrame 在页面处于后台时会挂起,等页面激活了才会重新启动

2.2.3 微任务

我们知道微任务也参与了事件循环,那么为什么不用微任务呢?\
原因很简单,因为微任务有几个问题:

  1. 执行顺序不可控:微任务之间没有优先级关系,先来了就先执行
  2. 无法中断:比如说我在当前的微任务里不想执行了,想要到下一个事件循环里的微任务再执行是做不到的
  3. 可能会阻塞渲染

2.2.4 宏任务

宏任务的特点:在事件循环的最开始执行\
浏览器里常见的宏任务有:

  • setTimeout
  • setInterval
  • MessageChannel

为什么 React 里面不是用 setTimeout 把 task 放到宏任务里呢?原因是,当 setTimeout 嵌套调用被安排了 5 次之后,浏览器就会强制执行 4ms 的最小超时。

其实这个 4ms 的间隔是无法接受的,因为 React 设置的分片时间也就 5ms。
同样的,setInterval 也会有类似的问题。

2.2.4.1 MessageChannel

一方面 MessageChannel 使用的是消息通道机制,比 setTimeout 要更快并且基本没有延迟:

const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
  port.postMessage(null);
};

另外一方面,浏览器兼容性也是非常好,基本上都是支持的:

因此,最终 React 选择使用 MessageChannel 来实现 Scheduler 也就不奇怪了。

2.2.5 总结

调度方式 描述 优缺点
requestIdleCallback 浏览器空闲时执行的回调 ❌ 不确定性高:可能根本不触发❌ 浏览器兼容性差(低端机、老浏览器)
requestAnimationFrame 渲染前调用,适合做视觉同步 ❌ 只能每帧一次,受帧率限制❌ 页面隐藏时会挂起,调度中断❌ 与 setTimeout 组合不可控
微任务(Promise.then) 每个宏任务后立即执行 ❌ 无法中断:一旦开始就必须执行完❌ 无优先级控制❌ 会阻塞主线程、渲染卡顿
setTimeout / setInterval 传统的异步任务调度方式(宏任务) ❌ 最小延迟 ≥ 4ms(嵌套 5 层以上)❌ 精度不够,无法满足时间片(如 5ms)要求
MessageChannel ✅ 现代浏览器提供的快速任务通道 ✅ 几乎无延迟(比 setTimeout 快)✅ 支持跨标签页、后台运行✅ 可搭配 performance.now() 实现时间切片✅ 浏览器兼容性非常好

3 源码阅读

3.1 初始化

unstable_scheduleCallback 是一个入口函数,它的参数包含:

  • priorityLevel:优先级
  • callback:回调任务
  • options

    • delay: 延迟

3.1.1 根据优先级分配超时时间

一开始就会根据 priorityLevel 来分配 timeout,优先级越高 timeout 越小。换句话说,优先级越高就要越早执行,否则就会超时。

var currentTime = getCurrentTime();

var startTime;
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:
    // Times out immediately
    timeout = -1;
    break;
  case UserBlockingPriority:
    // Eventually times out
    timeout = userBlockingPriorityTimeout;
    break;
  case IdlePriority:
    // Never times out
    timeout = maxSigned31BitInt;
    break;
  case LowPriority:
    // Eventually times out
    timeout = lowPriorityTimeout;
    break;
  case NormalPriority:
  default:
    // Eventually times out
    timeout = normalPriorityTimeout;
    break;
}

3.1.2 设置 task

后续这里会初始化一个 task,代码如下:

var expirationTime = startTime + timeout;

var newTask: Task = {
  id: taskIdCounter++,
  callback,
  priorityLevel,
  startTime,
  expirationTime,
  sortIndex: -1,
};

3.1.3 分配任务到对应的队列中

这一步是最后一步,也是最开始的一步:把 task 放到指定的队列中:

if (startTime > currentTime) {
  // This is a delayed task.
  newTask.sortIndex = startTime;
  push(timerQueue, newTask);
  if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
    // All tasks are delayed, and this is the task with the earliest delay.
    if (isHostTimeoutScheduled) {
      // Cancel an existing timeout.
      cancelHostTimeout();
    } else {
      isHostTimeoutScheduled = true;
    }
    // Schedule a timeout.
    requestHostTimeout(handleTimeout, startTime - currentTime);
  }
} else {
  newTask.sortIndex = expirationTime;
  push(taskQueue, newTask);
  if (enableProfiling) {
    markTaskStart(newTask, currentTime);
    newTask.isQueued = true;
  }
  // Schedule a host callback, if needed. If we're already performing work,
  // wait until the next time we yield.
  if (!isHostCallbackScheduled && !isPerformingWork) {
    isHostCallbackScheduled = true;
    requestHostCallback();
  }
}
  • 当 startTime > currentTime 时,newTask 会被 push 到 timerQueue 队列里,这时它的 sortIndexstartTime
  • 当 startTime <= currentTime 时,newTask 会被 push 到 taskQueue 队列里,这时它的 sortIndexexpirationTime

看到这里我们可以认为:

  • 即将开始的 task 会被放到 taskQueue,而还没开始的 task 会被放到 timerQueue
  • timerQueue 里的 task 优先级是按照 startTime 来标识的,越早开始优先级越高
  • taskQueue 里的 task 优先级是按照 expirationTime 来标识的,越早过期优先级越高

而前面提到 expirationTime = startTime + timeout,因而我们可以认为:\
即使有些 task 比较早开始,但是如果它本身的优先级不高,最终在 taskQueue 里的优先级也会被排在其他 task 后面。

3.2 timerQueue

前面提到,timerQueue 主要是用来存放一些延迟的任务,主要和下面这段代码有关:

if (startTime > currentTime) {
  // This is a delayed task.
  newTask.sortIndex = startTime;
  push(timerQueue, newTask);
  if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
    // All tasks are delayed, and this is the task with the earliest delay.
    if (isHostTimeoutScheduled) {
      // Cancel an existing timeout.
      cancelHostTimeout();
    } else {
      isHostTimeoutScheduled = true;
    }
    // Schedule a timeout.
    requestHostTimeout(handleTimeout, startTime - currentTime);
  }
}

当且仅当 taskQueue 队列为空且 newTasktimerQueue 队头里时会执行 requestHostTimeout 方法。
并且呢,在执行 requestHostTimeout 之前,还会判断下 isHostTimeoutScheduled 是否为 true,如果为 true 就会调用 cancelHostTimeout

3.2.1 cancelHostTimeout

这个函数非常简单:

function cancelHostTimeout() {
  // $FlowFixMe[not-a-function] nullable value
  localClearTimeout(taskTimeoutID);
  taskTimeoutID = ((-1: any): TimeoutID);
}

其中 localClearTimeout 其实就是 clearTimeout

const localClearTimeout =
  typeof clearTimeout === 'function' ? clearTimeout : null;

3.2.2 requestHostTimeout

这个函数其实也是比较简单:

function requestHostTimeout(
  callback: (currentTime: number) => void,
  ms: number,
) {
  // $FlowFixMe[not-a-function] nullable value
  taskTimeoutID = localSetTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}

就是一个普通的 setTimeout 函数封装的。

3.2.3 handleTimeout

看完上面的代码,我们在回到这句代码:

requestHostTimeout(handleTimeout, startTime - currentTime);

很容易可以明白,它的作用其实就是在延迟 startTime - currentTime 之后,执行一下 handleTimeout 方法。

function handleTimeout(currentTime: number) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime);

  if (!isHostCallbackScheduled) {
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback();
    } else {
      const firstTimer = peek(timerQueue);
      if (firstTimer !== null) {
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

简单梳理下,它做了这些事情:

  1. 调用 advanceTimers 方法,并传入 currentTime
  2. 如果 taskQueue 队列不为空,执行 requestHostCallback 方法
  3. 否则,从 timerQueue 里取出一个 timer, 之后执行 requestHostTimeout 方法
3.2.3.1 advanceTimers

照例先看代码:

function advanceTimers(currentTime: number) {
  // Check for tasks that are no longer delayed and add them to the queue.
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {
      // Timer was cancelled.
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
      // Timer fired. Transfer to the task queue.
      pop(timerQueue);
      timer.sortIndex = timer.expirationTime;
      push(taskQueue, timer);
      if (enableProfiling) {
        markTaskStart(timer, currentTime);
        timer.isQueued = true;
      }
    } else {
      // Remaining timers are pending.
      return;
    }
    timer = peek(timerQueue);
  }
}

简单梳理下这里的逻辑:

  1. timerQueue 里取出一个 timer
  2. 如果 timer.callback 为空,那么就把它从 timerQueue 里移除
  3. 如果 startTime <= currentTime

    1. 说明 timer 即将开始了,将它从 timerQueue 里移除
    2. 设置 timer.sortIndex = timer.expirationTime
    3. 将它 push 到 taskQueue
  4. 否则直接结束循环
  5. 如果循环没有结束,那么继续从 timerQueue 里取出一个 timer 并重复以上步骤

总结一下 advanceTimers 的作用:

  • 去掉无效的 timer
  • 及时将到期的 timertimerQueue 放到 taskQueue
3.2.3.2 requestHostCallback

这个函数不复杂:

function requestHostCallback() {
  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}

里面调用的其实是 schedulePerformWorkUntilDeadline 这个方法:

let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
  // Node.js and old IE.
  // There's a few reasons for why we prefer setImmediate.
  //
  // Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
  // (Even though this is a DOM fork of the Scheduler, you could get here
  // with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
  // https://github.com/facebook/react/issues/20756
  //
  // But also, it runs earlier which is the semantic we want.
  // If other browsers ever implement it, it's better to use it.
  // Although both of these would be inferior to native scheduling.
  schedulePerformWorkUntilDeadline = () => {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
} else {
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = () => {
    // $FlowFixMe[not-a-function] nullable value
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

可以看到 schedulePerformWorkUntilDeadline 其实是做了一下兼容性处理:

  • 如果当前 Nodejs 或者老 IE,那么就用 localSetImmediate 方法
  • 如果当前环境支持 MessageChannel 那么就直接用它
  • 否则就退而求其次使用 setTimeout 方法
3.2.3.3 performWorkUntilDeadline

schedulePerformWorkUntilDeadline 方法里调的其实就是 performWorkUntilDeadline

const performWorkUntilDeadline = () => {
  if (enableRequestPaint) {
    needsPaint = false;
  }
  if (isMessageLoopRunning) {
    const currentTime = getCurrentTime();
    // Keep track of the start time so we can measure how long the main thread
    // has been blocked.
    startTime = currentTime;

    // If a scheduler task throws, exit the current browser task so the
    // error can be observed.
    //
    // Intentionally not using a try-catch, since that makes some debugging
    // techniques harder. Instead, if `flushWork` errors, then `hasMoreWork` will
    // remain true, and we'll continue the work loop.
    let hasMoreWork = true;
    try {
      hasMoreWork = flushWork(currentTime);
    } finally {
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
      }
    }
  }
};

梳理一下逻辑:

  • 调用 flushWork 并将返回值赋值给 hasMoreWork
  • 如果说 hasMoreWorktrue,那么就继续调用 schedulePerformWorkUntilDeadline 方法
  • 否则就把 isMessageLoopRunning 标记为 false
3.2.3.4 flushWork

flushWork 这段代码比较多,但是核心的其实只有两个地方,其他都是一些开关的逻辑:

function flushWork(initialTime: number) {
  if (enableProfiling) {
    markSchedulerUnsuspended(initialTime);
  }

  // We'll need a host callback the next time work is scheduled.
  isHostCallbackScheduled = false;
  if (isHostTimeoutScheduled) {
    // We scheduled a timeout but it's no longer needed. Cancel it.
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

  isPerformingWork = true;
  const previousPriorityLevel = currentPriorityLevel;
  try {
    if (enableProfiling) {
      try {
        return workLoop(initialTime);
      } catch (error) {
        if (currentTask !== null) {
          const currentTime = getCurrentTime();
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskErrored(currentTask, currentTime);
          // $FlowFixMe[incompatible-use] found when upgrading Flow
          currentTask.isQueued = false;
        }
        throw error;
      }
    } else {
      // No catch in prod code path.
      return workLoop(initialTime);
    }
  } finally {
    currentTask = null;
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false;
    if (enableProfiling) {
      const currentTime = getCurrentTime();
      markSchedulerSuspended(currentTime);
    }
  }
}

首先就是这一段:

// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
  // We scheduled a timeout but it's no longer needed. Cancel it.
  isHostTimeoutScheduled = false;
  cancelHostTimeout();
}

如果当前有定时器在跑的话,isHostTimeoutScheduled 直接标记成 false 并且调 cancelHostTimeout 函数来清除掉定时器。

其次就是这一段:

return workLoop(initialTime)
3.2.3.5 workLoop

这段代码非常多,可以先看一下:

function workLoop(initialTime: number) {
  let currentTime = initialTime;
  advanceTimers(currentTime);
  currentTask = peek(taskQueue);
  while (currentTask !== null) {
    if (!enableAlwaysYieldScheduler) {
      if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
        // This currentTask hasn't expired, and we've reached the deadline.
        break;
      }
    }
    // $FlowFixMe[incompatible-use] found when upgrading Flow
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentTask.callback = null;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      currentPriorityLevel = currentTask.priorityLevel;
      // $FlowFixMe[incompatible-use] found when upgrading Flow
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      if (enableProfiling) {
        // $FlowFixMe[incompatible-call] found when upgrading Flow
        markTaskRun(currentTask, currentTime);
      }
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      if (typeof continuationCallback === 'function') {
        // If a continuation is returned, immediately yield to the main thread
        // regardless of how much time is left in the current time slice.
        // $FlowFixMe[incompatible-use] found when upgrading Flow
        currentTask.callback = continuationCallback;
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskYield(currentTask, currentTime);
        }
        advanceTimers(currentTime);
        return true;
      } else {
        if (enableProfiling) {
          // $FlowFixMe[incompatible-call] found when upgrading Flow
          markTaskCompleted(currentTask, currentTime);
          // $FlowFixMe[incompatible-use] found when upgrading Flow
          currentTask.isQueued = false;
        }
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
        advanceTimers(currentTime);
      }
    } else {
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
    if (enableAlwaysYieldScheduler) {
      if (currentTask === null || currentTask.expirationTime > currentTime) {
        // This currentTask hasn't expired we yield to the browser task.
        break;
      }
    }
  }
  // Return whether there's additional work
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

这里大概的逻辑是:

  1. 调用 advanceTimers 及时将到时的 timer 放到 taskQueue
  2. taskQueue 里取出一个 task
  3. 如果当前的分片时间 >= 5ms,那么就立刻退出循环并返回 false
  4. 如果 task.callback 不是一个 function 就将它从 taskQueue 里移除掉
  5. 如果 task.callback 是一个 function,那么就调用一下 callback 方法并拿到 continuationCallback
  6. 如果 continuationCallback 是一个 function,无论分片时间还剩余多少,这时候就要立刻将线程交还给主线程,此时会直接退出循环并返回 true,表示还有 task 没有执行完
  7. 否则如果当前的 tasktaskQueue 堆顶的 task 为同一个,就执行一下 pop 去掉堆顶的 task
  8. 同时,再次调用一次 advanceTimers,及时将到时的 timer 放到 taskQueue
  9. 重新从 taskQueue 里取出 task 并重复以上步骤
  10. 循环结束后:
  • 如果 currentTask !== null 则返回 true,表示还有 task 没有执行完毕
  • 如果 currentTask == null,那么就从 timerQueue 里取出一个 timer,并调用 `
    requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime),最终返回 false,表示已经执行完所有 task`

3.2.4 小结

上面提到了好几个关键的方法:

  • cancelHostTimeout
  • requestHostTimeout
  • handleTimeout
  • advanceTimers
  • requestHostCallback
  • performWorkUntilDeadline
  • flushWork
  • workLoop

在最后我们再来梳理一遍 timerQueue 整体的逻辑:

  1. 满足 startTime > currentTimetask 会被 push 到 timerQueue 里,之后会有一个定时器来执行 handleTimeout 方法。如果在这之前已经有定时器在跑了,那就会把它取消掉。
  2. handleTimeout 这里的逻辑,相当于是有一个轮询会一直从 timerQueue 里找到到时的任务放到 taskQueue 里,一旦 taskQueue 不为空就会执行 requestHostCallback
  3. requestHostCallback 会调用 schedulePerformWorkUntilDeadline 并发出一个宏任务,通常我们理解它会在下一个事件循环的开头里执行。
  4. 在宏任务里的函数,会依次执行

    • performWorkUntilDeadline
    • flushWork
    • workLoop
  5. 最后在 workLoop 里会不断从 taskQueue 里拿出 task 来执行,执行之前如果发现分片时间 >= 5ms 就会立刻退出循环并返回 true。如果发现执行结果返回的是 continuationCallback 也同样会马上退出循环并返回 true
  6. 最后如果发现依然还有 task,就会继续调用 schedulePerformWorkUntilDeadline 并重复以上动作

3.3 taskQueue

入口这里的 taskQueue 逻辑主要是这些:

newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
  markTaskStart(newTask, currentTime);
  newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
  isHostCallbackScheduled = true;
  requestHostCallback();
}

可以看出这里就是做两件事情:

  1. task 放入 taskQueue
  2. 调用 requestHostCallback

requestHostCallback 这个函数的逻辑已经在上面说过了,这里就不再赘述了。

4. 总结

在 React 18 引入的 Concurrent 模式中,调度系统的核心实现依赖于 Scheduler 包。通过小顶堆对任务进行优先级排序,再结合浏览器的事件循环机制,尤其是高效且兼容性好的 MessageChannel,实现了可中断、可恢复的任务执行模型。本文从小顶堆的数据结构开始,逐步剖析了 Scheduler 的调度策略、延迟任务处理、任务执行分片以及整个调度链路中的关键方法与队列交互逻辑,帮助我们更清晰地理解 React 是如何「在合适的时间做合适的事情」的。

你可能感兴趣的:(【React 源码阅读】Scheduler)