Scheduler 的目的
React 实现 Scheuler 的目的是想要实现时间切片。
时间切片是指:将长任务拆分成多段,每次执行一小段的任务的操作。
Scheduler 的实现
React 利用 MessageChannel 创建出一个 port 实例,port 实例有两个属性 port1 和 port2。如果在 Scheduler 当中调用 port2.postMessage() 时,浏览器就会调用 port1.onmessage 方法绑定的回调函数然后执行它。绑定的这个函数的函数名是 performWorkUntilDeadline。
performWorkUntilDeadline 函数的主要工作是调用 scheduledHostCallback 方法。而在执行完 scheduledHostCallback 方法时它的执行结果为 true,那么会接着执行port2.postMessage ()。
这样就会继续通过 performWorkUntilDeadline 方法执行 scheduledHostCallback。
而这个 scheduledHostCallback 变量是通过执行 requestHostCallback 方法时赋值的 flushWork 方法。所以 scheduledHostCallback 就是 flushWork。执行 performWorkUntilDeadline 就是执行
flushWork。
scheduleCallback
Scheduler 会向外暴露一个方法: scheduleCallback。这个方法接收两个参数:优先级,回调函数。它的目的就是以什么样的优先级调用一个回调函数。
Scheduler 的优先级
Scheduler 定义了 5 中优先级:ImmediatePriority、UserBlockingPriority、NormalPriority、LowPriority、IdlePriority。优先级分别从高到低。
这些优先级分别对应 -1、250、5000、10000、1073741823(最大 31 位二进制整数)值。
scheduleCallback 的工作流程
调用 scheduleCallback 方法首先会创建 task 对象,包含:callback、priorityLevel、startTime 和 expirationTime。
- callback 对应第二个参数
- priorityLevel 对应第一个参数
- startTime 一般对应当前时间,如果宿主支持 performance 对象则通过 performance.now() 获取,否则将通过 Date.now() 获取
- expirationTime = startTime + timeout,timeout 对应第一个参数对应的值
所以 Scheduler 中优先级也表示当前回调函数的过期时间。优先级越高就会越快过期,优先级越低就会越慢过期。
taskQueue 和 timerQueue
那么当startTime大于currentTime就表示这个 task 还没有过期,Scheduler 会将这个未过期的任务 push 到 timerQueue 中,timerQueue 就是用来保存未过期任务的队列。
而 startTime 小于等于 currentTime 就表示这个 task 已经过期了,Scheduler 会将这个过期任务 push 到 taskQueue 中,taskQueue 就是用来保存过期任务的队列。
说是队列不够准确,taskQueue 和 timerQueue 都是小顶堆,小顶堆是一颗完全二叉树。堆顶存放的都是最快过期时间的任务。
那么既然有了 taskQueue 就需要函数来执行它。
这个任务就是 workLoop 方法,而 workLoop 方法是通过 flushWork 调用的, 即上面提到过的 performWorkUntilDeadline。
所以这样就捋出了一条完整的流水线:先执行 port2.postMessage -> performWorkUntilDeadline -> flushWork -> workLoop -> 执行 taskQueue -> callback。
workLoop 的工作流程是先调用advanceTimers从 timerQueue 中取出过期任务放到 taskQueue 中。然后取出 taskQueue 中堆顶的任务执行它的 callback 属性,即传入的回调函数。
这个回调函数通常指 performConcurrentWorkOnRoot,即并发模式下 render 阶段的入口函数。
而时间切片的特性就体现在了这里:将同步的更新变为异步可中断的更新。
shouldYield
通过performConcurrentWorkOnRoot方法会调用renderRootConcurrent,然后调用workLoopConcurrent。
在 workLoopConcurrent 方法中会通过 while 循环执行 performUnitOfWork 方法。while 条件语句当中会通过 shouldYield 方法执行结果判断是否继续执行 performUnitOfWork 方法。
shouldYield 正是通过 Scheduler 中提供的方法。目的是判断当前帧是否还有剩余时间来执行浏览器的渲染工作。如果有剩余时间 shouldYield 会返回 false,没有剩余时间会返回 true。
当返回 true 时就会暂停执行 performUnitOfWork 方法,实现暂停 render 阶段的执行。
shouldYield的实现原理就是判断 getCurrentTime() >= deadline ,deadline 在执行 performWorkUntilDeadline 时会被赋值 currentTime + yieldInterval(yieldInterval = 5)。
所以从这里可以看出 Scheduler 为 render 阶段的执行时间预留的时间就是 5ms,如果大于 5ms 就会暂停 performUnitOfWork 方法的执行。等到下一帧再继续执行 performUnitOfWork 方法的 render 阶段。
任务的暂停到恢复
当 performUnitOfWork 方法被中断后又是如何恢复执行的呢?
在 workLoop 方法中执行 callback 后,即 performConcurrentWorkOnRoot 方法,如果 performUnitOfWork 被中断了,则 performConcurrentWorkOnRoot 方法会将自己作为执行结果再return出去 。如果 performUnitOfWork 没有被中断则会 return null。所以 workLoop 会判断当前 callback 是否有返回值。如果有返回值则将该返回值再赋值给 callback 属性。如果没有返回值则将该回调函数对应的 task 从 taskQueue 中删除。
所以到这里应该明白了当前帧被中断的 performConcurrentWorkOnRoot方法,将在下一帧中等待 workLoop 方法的执行。
直到这个 performConcurrentWorkOnRoot 方法执行结束。否则 performConcurrentWorkOnRoot 对应的 task 将一直保留在 taskQueue 中等待 workLoop 方法的执行。
workLoop 方法的执行结果会根据 taskQueue 中是否还有过期的任务,如果还有过期任务则会 return true,否则将 return false。
因为 workLoop 方法是在 flushWork 方法中执行的。所以 flushWork 也会将 workLoop 方法的执行结果 return 出去当作自己的执行结果。
在 performWorkUntilDeadline 中会对 flushWork 方法的执行结果做判断。如果执行结果是 true,那么将继续执行 port2.postMessage。否则将 scheduledHostCallback置为空。
Scheduler 完整的执行流程
scheduleCallback(normalPriority, performConcurrentWorkOnRoot) ->
requestHostCallback ->
port2.postMessage ->
performWorkUntilDeadline ->
scheduledHostCallback ->
flushWork ->
workLoop ->
callback(performConcurrentWorkOnRoot)->
renderRootConcurrent ->
workLoopConcurrent ->
shouldYield 返回 true ->
中断 performUnitOfWork ->
performConcurrentWorkOnRoot 返回 performConcurrentWorkOnRoot.bind ->
workLoop 返回 true ->
flushWork 返回 true ->
performWorkUntilDeadline ->
port2.postMessage ->
... 重复上述流程 直到 performConcurrentWorkOnRoot 结束完成 返回 null ->
重置 scheduledHostCallback为 null ->
结束
思考
React 使用 MessageChannel 来实现时间切片特性的可能原因:MessageChannel 调度的回调函数是宏任务,这样当该回调函数执行结束之后可以将主线程的控制权交还给浏览器,方便浏览器执行渲染工作。
不用微任务的原因可能也是因为无法交还主线程的控制权,因为当前如果产生微任务的话,将一直执行微任务,直到微任务全部执行完。这样浏览器就一直没办法执行渲染的工作了。
不使用setTimeout的原因是因为 setTimeout 调用回调函数是有最小延迟的时间,会浪费宝贵的执行时间。
但是在 Scheduler 中也会判断,如果当前宿主环境不支持 MessageChannel,则会改为使用 setTimeout 。
标签:taskQueue,performConcurrentWorkOnRoot,流程,工作,Scheduler,workLoop,执行,方法 From: https://www.cnblogs.com/rocenjs/p/18364804