【React源码】Scheduler

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


先看下requestIdleCallback是什么

React中的调度算法和requestIdleCallback这个api息息相关。是利用浏览器一帧的剩余时间来执行优先级较低的任务

React为什么要重写requestIdleCallback

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

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

所以不仅API兼容一般,帧渲染能力一般,需求也不太符合。所以React团队自行实现。

React如何重写

  • 老版本:scheduler中采用了MessageChannel来实现requestIdleCallback,

当前环境下不支持MessageChannel就采用setTimeout

  • 新版本:优先使用setImmediate,如果没有,再使用MessageChannelgithubgitee

需要解决什么问题

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

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

调度优先级

在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

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享