【React源码】Scheduler(任务调度)

1,148 阅读5分钟

写在前面:这篇内容,更多像是随笔,有些内容只是给了一个入口,具体调度是怎么实现的,还是需要非常详细的去看源码!!!


一帧中js执行顺序

宏任务 -> 微任务 -> requestAnimationFrame -> 重排/重绘 -> requestIdleCallback

requestIdleCallback

React中的调度算法requestIdleCallback这个api息息相关。

  • requestIdleCallback:利用浏览器一帧的剩余时间来执行优先级较低的任务

为什么重写requestIdleCallback

其实这里用重写这个词非常不严谨,严格来说根本没有用requestIdleCallback。应该是为什么不用requestIdleCallback才对

requestIdleCallback缺陷

这里就是要讨论reqeustIdleCallback的缺陷了

  • 还是一个实现的API,兼容性很差
  • requestIdleCallback的FPS只有20ms,正常情况下渲染一帧时长控制在16.67ms,该时间是高于页面流畅的需求的
  • requestIdleCallback的定位是处理不重要不紧急的低优先级任务。和React可能不太符(React渲染内容,并非是不紧急不重要) 所以不仅API兼容一般,帧渲染能力一般,需求也不太符合。所以React团队自行实现。

如何重写

  • 老版本:scheduler中采用了MessageChannel来实现requestIdleCallback, 当前环境下不支持MessageChannel就采用setTimeout
  • 新版本:优先使用setImmediate,如果没有,再使用MessageChannel - githubgitee

需要解决什么问题

想要实现requestIdleCallback的处理,需要解决两个问题:

  • 如何判断一帧是否有空闲?
  • 如果有了空闲,在一帧中哪里去执行任务?
  • 到达deadline后如何暂停?

判断当前帧是否有空闲时间

如果有空闲,在一帧中哪里去执行任务?

我们已经知道,是生成一个宏任务来实现任务调度,实现代码如下

let schedulePerformWorkUntilDeadline;
if (typeof setImmediate === 'function') {
  schedulePerformWorkUntilDeadline = () => {
    setImmediate(performWorkUntilDeadline);
  };
} else {
  const channel = new MessageChannel();
  const port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;
  schedulePerformWorkUntilDeadline = () => {
    port.postMessage(null);
  };
}

如上:可以通过执行schedulePerformWorkUntilDeadline函数主动触发任务执行

如何暂停

上面我们已知是通过deadline来判断当前帧是否有时间,是否还需要继续执行的。那么暂停的依据:

// shouldYield函数
if (currentTime >= deadline) {
    // 时间到,暂停让出执行权
    return true;
}

如何继续(TODO)

TODO 暂时也没懂

​ 在performConcurrentWorkOnRoot函数的结尾有这样一个判断,如果callbackNode等于originalCallbackNode那就恢复任务的执行

if (root.callbackNode === originalCallbackNode) {
  // The task node scheduled for this root is the same one that's
  // currently executed. Need to return a continuation.
  return performConcurrentWorkOnRoot.bind(null, root);
}

调度优先级

在Scheduler中有两个函数可以创建具有优先级的任务

runWithPriority

runWithPriority

scheduleCallback

scheduleCallback

111.jpg

  • 以一个优先级注册callback,在适当的时机执行
  • 优先级意味着过期时间,优先级越高priorityLevel就越小,过期时间离当前时间就越近(如上图)
var expirationTime = startTime + timeout

如立即执行:IMMEDIATE_PRIORITY_TIMEOUT = -1

var expirationTime(到期时间) = startTime + (-1).就小于当前时间了,所以要立即执行

  • 调度的过程中用到了小顶堆,所以我们在O(1)的复杂度找到优先级最高的task
  • timerQueue:未过期的任务
  • taskQueue:过期的任务
单个任务数据结构
  /** 任务对象 */
  var newTask = {
    id: taskIdCounter++, // id,表示任务数
    callback, // 当前任务真正需要做的事情
    priorityLevel, // 优先级
    startTime, // 开始时间
    expirationTime, // 过期时间,用于比较
    sortIndex: -1, // 用于比较任务优先级的关键地方,后面回去更新
  };

相关常量和函数

【常量】yieldInterval - 时间片

yieldInterval

  • 每一帧的时间片长度,默认是5ms,会通过当前浏览器的fps来计算时间片。由forceFrameRate修改

【常量】deadline - 截止时间

【函数】requestHostCallback

requestHostCallback

  • 类似于 requestIdleCallback
  • 通过执行 schedulePerformWorkUntilDeadline 函数,来实现对函数performWorkUntilDeadline的执行触发,更新当前帧下一帧的结束时间,也就是deadline常量

再梳理下流程:

requestHostCallback -> schedulePerformWorkUntilDeadline -> performWorkUntilDeadline

【函数】shouldYieldToHost

shouldYieldToHost

  • 主要作用是判断当前时间是否已经超过deadline。如果超过了,返回true,其他地方就可以中断任务

【文件】SchedulerPriorities

SchedulerPriorities.js

任务调度是按照任务优先级调用执行,这里就是定义的任务优先级文件

【常量】currentPriorityLevel - 当前任务优先级

currentPriorityLevel

在runWithPriority方法中会修改,同时这个函数执行接收到的回调函数时,会拿到当前的currentPriorityLevel

【常量】taskQueue - 过期任务

过期的任务

【常量】timerQueue - 未过期任务

未过期的任务

Q & A

为什么是MessageChannel?

其实这个问题,更严谨来说,为什么是宏任务(MessageChannel、setImmediate、setTimeout)

Scheduler需要满足以下功能点:

  • 暂停JS执行,将主线程交还给浏览器,让浏览器有机会更新页面。也就是中断
  • 在未来某个时刻继续调度任务,执行上次还没有完成的任务

要满足上面亮点就需要调度一个宏任务,因为宏任务是在下次事件循环中执行,不会阻塞本次页面更新。而微任务是在本次页面更新前执行,与同步执行无异,不会让出主线程

为什么不是setTimeout(fn, 0)

因为递归执行setTimeout(fn, 0)时,最后间隔时间会变成4ms(自己试验,甚至不止4ms),而不是最初的1ms

var count = 0

var startVal = +new Date()
console.log("start time", 0, 0)
function func() {
  setTimeout(() => {
    console.log("exec time", ++count, +new Date() - startVal)
    if (count === 50) {
      return
    }
    func()
  }, 0)
}

func()

settimeout.jpg

为什么不用rAF()

  • 如果上次任务调度不是rAF()触发的(scheduler.scheduleTask()),将导致在当前更新前进行两次任务调度(两次的原因:如果在rAF()的回调中再调用rAF(),会将第二次的rAF()的回调放到下一帧前执行,而不是当前帧)
  • 页面更新的时间不确定,如果浏览器间隔10ms才更新页面,那么这10ms就浪费了

现有WEB技术中并没有规定浏览器应该什么时候更新页面,通常认为是在一次宏任务完成之后,浏览器自行判断当前是否应该更新页面。如果需要更新页面,则执行rAF()的回调并更新页面,否则就执行下一个宏任务

学习地址

地址1地址2