React 的调度系统 Scheduler

网站建设3年前发布
54 0 0

20230306141247e1f7672503bb9cd60a55714781db8b36e462d9140,React 使用了全新的 Fiber 架构,将原本需要一次性递归找出所有的改变,并一次性更新真实 DOM 的流程,改成通过时间分片,先分成一个个小的异步任务在空闲时间找出改变,最后一次性更新 DOM。,这里需要使用调度器,在浏览器空闲的时候去做这些异步小任务。,做这个调度工作的在 React 中叫做 Scheduler(调度器)模块。,其实浏览器是提供一个 requestIdleCallback 的方法,让我们可以在浏览器空闲的时去调用传入去的回调函数。但因为兼容性不好,给的优先级可能太低,执行是在渲染帧执行等缺点。,所以 React 实现了 requestIdleCallback 的替代方案,也就是这个 Scheduler。它的底层是 基于 MessageChannel 的。,选择 MessageChannel 的原因,是首先异步得是个宏任务,因为宏任务中会在下次事件循环中执行,不会阻塞当前页面的更新。MessageChannel 是一个宏任务。,没选常见的 setTimeout,是因为MessageChannel 能较快执行,在 0~1ms 内触发,像 setTimeout 即便设置 timeout 为 0 还是需要 4~5ms。相同时间下,MessageChannel 能够完成更多的任务。,若浏览器不支持 MessageChannel,还是得降级为 setTimeout。,其实如果 setImmediate 存在的话,会优先使用 setImmediate,但它只在少量环境(比如 IE 的低版本、Node.js)中存在。,逻辑是在 packages/scheduler/src/forks/Scheduler.js 中实现的:,另外,也没有选择使用 requestAnimationFrame,是因为它的机制比较特别,是在更新页面前执行,但更新页面的时机并没有规定,执行时机并不稳定。,requestHostCallback 方法,用于请求宿主(指浏览器)去执行函数。该方法会将传入的函数保存起来到 scheduledHostCallback 上,,然后调用 schedulePerformWorkUntilDeadline 方法。,schedulePerformWorkUntilDeadline 方法一调用,就停不下来了。,它会异步调用 performWorkUntilDeadline,后者又调用回 schedulePerformWorkUntilDeadline,最终实现 不断地异步循环执行 performWorkUntilDeadline。,isMessageLoopRunning 是一个 flag,表示是否正在走循环。防止同一时间调用多次 schedulePerformWorkUntilDeadline。,我们在 React 项目启动后,执行一个更新操作,会调用 ensureRootIsScheduled 方法。,该方法有很多分支,最终会根据条件调用:,performSyncWorkOnRoot 最终会执行重要的 workLoopSync 方法:,workInProgress 表示一个需要进行处理的 FiberNode。,performUnitOfWork 方法用于处理一个 workInProgress,进行调和操作,计算出新的 fiberNode。,同样,performConcurrentWorkOnRoot 最终会执行重要的 workLoopConcurrent 方法。,和 workLoopSync 很相似,但循环条件里多了一个来自 Scheduler 的 shouldYield() 决定是否将进程让出给浏览器,这样就能做到中断 Fiber 的调和阶段,做到时间分片。,上面的 workLoopSync 和 workLoopConcurrent 都是通过 scheduleCallback 去调度的。,scheduleCallback 方法传入优先级 priorityLevel、需要指定的回调函数 callback ,以及一个可选项 options。,scheduleCallback 的实现如下(做了简化):,push / peek / pop 这些是 scheduler 提供的操作 优先级队列 的操作方法。,优先级队列的底层实现是小顶堆,实现原理不展开讲。我们只需要记住优先级队列的特性:就是出队的时候,会取优先级最高的任务。在 scheduler 中,sortIndex 最小的任务的优先级最高。,push(queue, task)​ 表示入队,加一个新任务;peek(queue)​ 表示得到最高优先级(不出队);pop(queue) 表示将最高优先级任务出队。,taskQueue 为逾期的任务队列,需要赶紧执行。新生成的任务(没有设置 options.delay)会放到 taskQueue,并以 expirationTime 作为优先级(sortIndex)来比较。,timerQueue 是还没逾期的任务队列,以 startTime 作为优先级来比较。如果逾期了,就会 取出放到 taskQueue 里。,requestHostTimeout 其实就是 setTimeout 定时器的简单封装,在 newTask 过期的时间点(startTime - currentTime 后)执行 handleTimeout。,handleTimeout 下会调用 advanceTimers 方法,根据当前时间要将 timerTask 中逾期的任务搬到 taskQueue 下。,(advanceTimers 这个方法会在多个位置被调用。搬一搬,更健康),搬完后,看看 taskQueue 有没有任务要做,有的话就调用 flushWork 清空 taskQueue 任务。没有的话看看有没有未逾期任务,用定时器在它过期的时间点再递归执行 handleTimeout。,flushWork 会 调用 workLoop。flushWork 还需要做一些额外的修改模块文件变量的操作。,workLoop  会不停地从 taskQueue 取出任务来执行。其核心逻辑为:,上面的循环并不是一直会执行到 currentTask 为 null 为止,在必要的时候还是会跳出的。我们是通过 shouldYieldToHost 方法判断是否要跳出。,此外,Fiber 异步更新的 workLoopConcurrent 方法用到的 shouldYield,其实就是这个 shouldYieldToHost。,shouldYieldToHost 核心实现:,计算经过的时间,如果小于帧间隔时间(frameInterval,通常为 5ms),不需要让出进程,否则让出。,startTime 是模块文件的最外层变量,会在 performWorkUntilDeadline 方法中赋值,也就是任务开始调度的时候。,试着画一下 Scheduler 的调度流程图。,2023030612394083e92d88563b0415b92734a0db02794cd3c428803,Scheduler 一套下来还是挺复杂的。,首先是 Scheduler 底层大多数情况下会使用 MessageChannel,作为循环执行异步任务的能力。通过它来不断地执行任务队列中的任务。,任务队列是特殊的优先级队列,特性是出队时,拿到优先级最高的任务(在 Scheduler 中对比的是 sortIndex,值是一个时间戳)。,任务队列在 Scheduler 中有两种。一种是逾期任务 taskQueue,需要赶紧执行,另一种是延期任务 timerQueue,还不到时间执行。Scheduler 会根据当前时间,将逾期的 timerQueue 任务放到 taskQueue 中,然后从 taskQueue 取出优先级最高的任务去执行。,Scheduler 向外暴露 scheduleCallback 方法,该方法接受一个优先级和一个函数(就是任务),对于 React 来说,它通常是 workLoopSync 或 workLoopConcurrent。,scheduleCallback 会设置新任务的过期时间(根据优先级),并判断是否为延时任务(根据 options.delay)决定放入哪个任务队列中。然后启用循环执行异步任务,不断地清空执行 taskQueue。,Scheduler 也向外暴露了 shouldYield,通过它可以知道是否执行时间过长,应该让出进程给浏览器。该方法同时也在 Scheduler 内部的循环执行异步任务中作为一种打断循环的判断条件。,React 的并发模式下,可以用它作为暂停调和阶段的依据。

© 版权声明

相关文章