likes
comments
collection
share

React 之 Scheduler 源码解读(下)

作者站长头像
站长
· 阅读数 10

本文为稀土掘金技术社区首发签约文章,14 天内禁止转载,14 天后未获授权禁止转载,侵权必究!

本篇是 React 基础与进阶系列第 14 篇,关注专栏

前言

本篇我们接着《React 之 Scheduler 源码解读(上)》,讲解延时任务的执行源码。

scheduleCallback

依然从 unstable_scheduleCallback这个入口函数说起:

var isHostTimeoutScheduled = false;

function unstable_scheduleCallback(priorityLevel, callback, options) {
  // ...
  // 如果是延时任务,将其放到 timerQueue
  if (startTime > currentTime) {
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);
    // 任务列表空了,而这是最早的延时任务
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      if (isHostTimeoutScheduled) {
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      }
      // 安排调度
      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  }
    // 如果是普通任务,就将其放到 taskQueue 
  else {
  	// ...
  }

  return newTask;
}

普通任务在创建后,会放入 taskQueue 中,直接安排调度,但具体任务时候执行,则要看调度器 Scheduler 的安排。

而所谓延时任务,指的是延时安排调度的任务,它会有一个指定的 delay 值,表示具体延时多久,通过 delay + currentTime,我们可以算出安排调度的具体时间,也就是 startTime。

对于延时任务,我们会将其放入 timerQueue 队列。

然后我们进行了判断:

if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
  if (isHostTimeoutScheduled) {
    cancelHostTimeout();
  } else {
    isHostTimeoutScheduled = true;
  }
}

如果 taskQueue 没有任务,并且创建的这个任务就是最早的延时任务,那就执行 cancelHostTimeout,这样做保证了只有一个 requestHostTimeout 在执行,那 requestHostTimeoutcancelHostTimeout 做了什么呢?

requestHostTimeout

let taskTimeoutID = -1;

function requestHostTimeout(callback, ms) {
  taskTimeoutID = setTimeout(() => {
    callback(getCurrentTime());
  }, ms);
}

requestHostTimeout 就是一个 setTimeout 的封装,所谓延时任务,就是一个延时安排调度的任务,怎么保证在延时时间达到后立刻安排调度呢,React 就用了 setTimeout,计算 startTime - currentTime 来实现,我们也可以想出,handleTimeout 的作用就是安排调度。

cancelHostTimeout 代码我们也很容易想到了:

cancelHostTimeout

function cancelHostTimeout() {
  clearTimeout(taskTimeoutID);
  taskTimeoutID = -1;
}

结合 unstable_scheduleCallbackrequestHostTimeoutcancelHostTimeout 的代码,我们可以了解到:

在 Scheduler 中,最多只有一个定时器在执行(requestHostTimeout),时间为所有延时任务中延时时间最小的那个,如果创建的新任务是最小的那个,那就取消掉之前的,使用新任务的延时时间再创建一个定时器,定时器到期后,我们会将该任务安排调度(handleTimeout)

但这个逻辑只在 taskQueue 没有任务的时候,如果 taskQueue 有任务呢?

如果 taskQueue 有任务,在每个任务完成的时候,React 都会调用 advanceTimers ,检查 timerQueue 中到期的延时任务,将其转移到 taskQueue 中,所以没有必要再检查一遍了。

总结一下:如果 taskQueue 为空,我们的延时任务会创建最多一个定时器,在定时器到期后,将该任务安排调度(将任务添加到 taskQueue 中)。如果 taskQueue 列表不为空,我们在每个普通任务执行完后都会检查是否有任务到期了,然后将到期的任务添加到 taskQueue 中。

但这个逻辑里有一个漏洞:

我们新添加一个普通任务,假设该任务执行时间为 5ms,再添加一个延时任务,delay 为 10ms。

因为创建延时任务的时候 taskQueue 中有值,所以不会创建定时器,当普通任务执行完毕后,我们执行 advanceTimers,因为延时任务没有到期,所以也不会添加到 taskQueue 中,那么这个延时任务就不会有定时器让它准时进入调度。如果没有新的任务出现,它永远都不会执行。

所以在 workLoop 函数的源码中,有这样一段代码:

function workLoop(hasTimeRemaining, initialTime) {
  advanceTimers(currentTime);
	
  // ...
  
  if (currentTask !== null) {
    return true;
  } else {
    const firstTimer = peek(timerQueue);
    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }
    return false;
  }
}

我们执行完任务,如果 taskQueue 为空,并且 timerQueue 中还有任务,那我们就再创建一个定时器。

handleTimeout

接下来我们看看 handleTimeout 的源码,这个函数执行的时候,该延时任务已经到期:

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

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

可以看到首先调用了 advanceTimers,将到期的延时任务转移到 taskQueue 中。

如果 taskQueue 不为空,那就执行 requestHostCallback,告诉浏览器,等空了就干活,继续遍历执行 taskQueue 中的任务。

而如果 taskQueue 为空,嗯?为什么会为空呢?

既然 handleTimeout 执行了,说明这个延时任务一定是到期了,我们执行 advanceTimers,taskQueue 中一定有任务,这里肯定不为空呀。

这里我们要考虑一种情况,那就是延时任务可能被取消了,但这个取消不是 cancelHostTimeout,执行 cancelHostTimeout,我们只是移除了定时器,延时任务还是保存在 timerQueue 中,我们说的取消,是真正的取消,取消的方式是将任务对象 task 的 callback 函数置为 null。当 React 执行 advanceTimers 的时候,advanceTimers 会判断 callback 函数的值,如果为空,表示完成或者清除,那就从任务列表中移除掉。

所以如果我们发起一个延时任务,然后将该延时任务取消,当执行 handleTimeout 的时候,peek(taskQueue) 的结果就会为空,此时怎么解决呢?

很简单,那就根据现有的 timerQueue 中的任务,新开启一个定时器好了。

总结:延时任务流程

当我们创建一个延时任务后,我们将其添加到 timerQueue 中,我们使用 requestHostTimeout 来安排调度,requestHostTimeout 本质是一个 setTimeout,当时间到期后,执行 handleTimeout,将到期的任务转移到 taskQueue,然后按照普通任务的执行流程走。

flushWork 中的 cancelHostTimeout

function flushWork(hasTimeRemaining: boolean, initialTime: number) {
  if (isHostTimeoutScheduled) {
    isHostTimeoutScheduled = false;
    cancelHostTimeout();
  }

	// ...
  return workLoop(hasTimeRemaining, initialTime);
}

我们在执行 flushWork 的时候,如果有正在执行的定时器,我们会执行 cancelHostTimeout 取消定时器,这里为什么要取消呢?

定时器的目的表面上是为了保证最早的延时任务准时安排调度,实际上是为了保证 timerQueue 中的任务都能被执行。定时器到期后,我们会执行 advanceTimers 和 flushWork,flushWork 中会执行 workLoop,workLoop 中会将 taskQueue 中的任务不断执行,当 taskQueue 执行完毕后,workLoop 会选择 timerQueue 中的最早的任务重新设置一个定时器。所以如果 flushWork 执行了,定时器也就没有必要了,所以可以取消了。

至此,React 的 Scheduler 的源码解读第一遍就结束了,接下来我们会补充讲解 Scheduler 的细节实现、提供可供直接使用的源码版本,原理总结,帮助大家更好的认识 Scheduler。

React 系列

  1. React 之 createElement 源码解读
  2. React 之元素与组件的区别
  3. React 之 Refs 的使用和 forwardRef 的源码解读
  4. React 之 Context 的变迁与背后实现
  5. React 之 Race Condition
  6. React 之 Suspense
  7. React 之从视觉暂留到 FPS、刷新率再到显卡、垂直同步再到16ms的故事
  8. React 之 requestAnimationFrame 执行机制探索
  9. React 之 requestIdleCallback 来了解一下
  10. React 之从 requestIdleCallback 到时间切片
  11. React 之最小堆(min heap)
  12. React 之如何调试源码
  13. 《React 之 Scheduler 源码解读(上)》

React 系列的预热系列,带大家从源码的角度深入理解 React 的各个 API 和执行过程,全目录不知道多少篇,预计写个 50 篇吧。