这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
这一节来看调度器,我们在前面 createDeferred 部分接触过 requestCallback 函数,这部分就属于调度器,solid 的调度器位于 scheduler 文件中。除了 createDeferred 外,solid 提供了一个 public API enableScheduling 用来开启调度器,这部分目前是实验性 API。
了解 react 一定会听过 scheduler 的概念,react 的特色功能时间切片就是 scheduler 实现的,在空闲时进行更新,把长任务切分,使得浏览器随时都可以有响应,提升用户体验。而 solid.js 与此不同,它是定向处理更新的,因此 solid 默认使用同步的调度策略依旧可以有很好的性能表现。虽然不是必须的,但是 solid 也是可以实现调度机制的,这里的 scheduler 是提供了这样一个可选的能力。
enableScheduling 打开后,会创建一个 Scheduler 对象,在 Transition 执行的过程中,执行各类 Update 时,如果当前处于 Transition 执行状态,这里就会将任务推入 Updates 队列而非立即执行。在 completeUpdates 阶段,如果有 Transition 在执行,这里就会调用 scheduleQueue 来更新:
function completeUpdates(wait: boolean) {
if (Updates) {
if (Scheduler && Transition && Transition.running) scheduleQueue(Updates);
else runQueue(Updates);
Updates = null;
}
// others ...
}
这里的 scheduleQueue 就是把待执行的队列放入调度器中执行,这个调度器是在 enableScheduling 中传入的,默认是 requestCallback,目前 solid 只有这一种内置调度器。
这里调度器实现的核心能力就是打断和恢复,一个长任务执行一部分后暂停,浏览器下一个空闲时间继续执行。在浏览器的事件循环中有宏任务和微任务两种调度策略,微任务是只要有就一直执行,直到队列清空,宏任务是每次循环执行一个,之后浏览器继续做其他事情直到下次循环,因此这里的需求显然是需要一个宏任务调度。
浏览器中我们可以用 setTimeout 创建一个宏任务,但是 setTimeout 这类方法不够稳定,后面实际的间隔会变长,实际效果并不好,因此 solid 采用的是 MessageChannel。具体细节推荐阅读 React Scheduler 为什么使用 MessageChannel 实现 - 掘金 (juejin.cn),调度是一个通用能力,react 和其他框架都是同样道理。
MessageChannel 原本是浏览器提供的通信接口,可以用于 WebWorker 之间通信,它可以返回两个 port,分别传入两个 worker,这样两个 worker 就可以直接通信,不需要经过主线程中转。这里使用 MessageChannel 显然不是为了应用它的功能,而是因为 MessageChannel 可以创建一个宏任务。
function setupScheduler() {
const channel = new MessageChannel(),
port = channel.port2;
scheduleCallback = () => port.postMessage(null);
channel.port1.onmessage = () => {
// ...
}
// ...
}
核心逻辑就是这里,每次调度结束就调用一下 port.postMessage(null) 这样就把下次调度放入宏任务队列中,下次执行时发现有没做完的任务继续做,这样整个调度更新的流程就被切分为小的任务,这就是调度器的实现原理。