React 之 Scheduler 源码解读(下)

简介: 本篇我们接着《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。


目录
相关文章
|
6月前
|
Web App开发 存储 前端开发
React 之 Scheduler 源码中的三个小知识点,看看你知不知道?
本篇补充讲解 Scheduler 源码中的三个小知识点。
119 0
|
1月前
|
移动开发 JSON 数据可视化
精选八款包括可视化CMS,jquery可视化表单,vue可视化拖拉,react可视化源码
精选八款包括可视化CMS,jquery可视化表单,vue可视化拖拉,react可视化源码
47 0
|
5月前
|
前端开发 资源调度
如何本地 Debug React 源码
如何本地 Debug React 源码
40 3
|
6月前
|
JSON 前端开发 JavaScript
React源码解析-JSX
React源码解析-JSX
111 1
|
6月前
|
前端开发 JavaScript 测试技术
10个yyds的Vue、React源码解析开源项目
10个yyds的Vue、React源码解析开源项目
|
6月前
|
前端开发 调度
高端操作:把 React Scheduler 掏出来单独用
高端操作:把 React Scheduler 掏出来单独用
|
6月前
|
前端开发 调度
300 行代码实现 React 的调度器 Scheduler
说是实现,但其实我们只是在 React Scheduler 源码的基础上进行了简化,省略掉一些繁琐的细节,添加了丰富的注释,保证代码可直接执行。 大家可以复制代码到编辑器中,直接运行,非常适合学习 React 源码用。
71 0
|
6月前
|
设计模式 前端开发 数据可视化
【第4期】一文了解React UI 组件库
【第4期】一文了解React UI 组件库
357 0
|
6月前
|
存储 前端开发 JavaScript
【第34期】一文学会React组件传值
【第34期】一文学会React组件传值
74 0
|
6月前
|
资源调度 前端开发 JavaScript
React 的antd-mobile 组件库,嵌套路由
React 的antd-mobile 组件库,嵌套路由
121 0