diff --git a/packages/rspress-site/docs/guide/scheduler.mdx b/packages/rspress-site/docs/guide/scheduler.mdx new file mode 100644 index 0000000..6c19d18 --- /dev/null +++ b/packages/rspress-site/docs/guide/scheduler.mdx @@ -0,0 +1,415 @@ +# Scheduler 调度器 + +Scheduler(调度器)负责管理任务的调度和优先级。定义了任务调度的逻辑,包括任务的优先级、任务队列的管理等。是 React 中非常重要的一个组件。 + +React 并没有将本身的任务优先级调度和 Scheduler 耦合在一起,为了保证其通用性,将 Scheduler 作为一个独立的包存在。可以直接安装 [Scheduler](https://www.npmjs.com/package/scheduler) 包来使用。 + +:::tip +了解事件循环 EventLoop 能更好掌握本章内容,可参考笔者以前写的[消息队列与事件循环](https://mp.weixin.qq.com/s/vzR6Ya4MVQJNijPodhBLww) +::: + +## 举一个例子 + +在深入到源码之前,先来看看:如果要自己实现一套任务调度机制,应该怎么做? + +任务调度器的本质就是将任务按照优先级进行排序,然后按照优先级依次执行任务。流程示意图如下所示: + +![](/d2/mini-scheduler-v1.svg) + +1. `schedule` 负责创建和 push 任务 +2. push 到 TaskQueue 中的任务按照优先级和 id 进行排序 +3. loop 所有的任务队列,并依次执行 + +关键代码如下: + +```ts +const taskQueue = []; + +let id = 0; + +function schedule(execute: () => void, priority: number) { + const task = { + id: id++, + execute, + priority + }; + push(task); + console.log("schedule task: ", task); + perform(); +} + +function perform() { + while (taskQueue.length) { + const task = peek(); + console.log("perform task: ", task); + task.execute(); + } +} + +function push(task) { + taskQueue.push(task); + taskQueue.sort((a, b) => { + // 先比较优先级,如果优先级相同,再比较 id + const diff = a.priority - b.priority; + return diff !== 0 ? diff : a.id - b.id; + }); +} + +function peek() { + return taskQueue.shift(); +} +``` + +最最基础版的 `mini-scheduler` 就已经实现完成了,可通过下面的 Demo 体验: + +```tsx preview +/** + * mini-scheduler + */ +const taskQueue = []; + +let id = 0; + +function schedule(execute: () => void, priority: number) { + const task = { + id: id++, + execute, + priority + }; + push(task); + console.log("schedule task: ", task); + perform(); +} + +function perform() { + while (taskQueue.length) { + const task = peek(); + console.log("perform task: ", task); + task.execute(); + } +} + +function push(task) { + taskQueue.push(task); + taskQueue.sort((a, b) => { + // 先比较优先级,如果优先级相同,再比较 id + const diff = a.priority - b.priority; + return diff !== 0 ? diff : a.id - b.id; + }); +} + +function peek() { + return taskQueue.shift(); +} + +/** + * demo + */ +export default () => { + const onHighPriorityClick = () => { + const execute = () => { + const container = document.getElementById("mini-scheduler"); + if (container) { + const node = document.createElement("div"); + node.innerText += Date.now(); + container.appendChild(node); + } + }; + + schedule(execute, /* 0 代表高优先级*/ 0); + schedule(execute, /* 0 代表高优先级*/ 0); + }; + + const onLowPriorityClick = () => { + const execute = () => { + const container = document.getElementById("mini-scheduler"); + if (container) { + container?.classList.toggle("bg-sky-500"); + } + }; + + schedule(execute, /* 1 代表低优先级*/ 1); + }; + + const onMixPriorityClick = () => { + schedule(() => { + const container = document.getElementById("mini-scheduler"); + if (container) { + const node = document.createElement("div"); + node.innerText += "低优"; + container.appendChild(node); + } + }, 1); + + schedule(() => { + const container = document.getElementById("mini-scheduler"); + if (container) { + const node = document.createElement("div"); + node.innerText += "高优"; + container.appendChild(node); + } + }, 0); + }; + + return ( +
+
+ + + +
+
+
+ ); +}; +``` + +但这个版本有两个最大的问题: + +1. `schedule` 任务后,就立即开始了 loop 执行,现实中会延后执行 +2. 单纯的按照 `priority` 排序,如果在期间,不断有高优任务插入,低优任务就会一直被阻塞,而被“饿死” + +v2 流程示意图如下所示: +![](/d2/mini-scheduler-v2.svg) + +关键代码如下: + +```ts +const taskQueue = []; + +let id = 0; + +function schedule(execute: () => void, priority: number) { + // 根据优先级计算出任务的过期时间,并以过期时间作为排序依据 + // 即使在插入底优任务后,不断有高优任务插入,也能保证随着时间的推移,底优任务被排在第一位 + let timeout; + if (priority === 0) { + timeout = 0; + } else { + timeout = 100; + } + const expirationTime = Date.now() + timeout; + const task = { + id: id++, + execute, + expirationTime + }; + push(task); + console.log("schedule task: ", task); + schedulePerform(); +} + +// 标识 loop 是否在执行中 +let isLoopRunning = false; + +function schedulePerform() { + if (!isLoopRunning) { + isLoopRunning = true; + // 暂时用 setTimeout 做延迟处理 + setTimeout(() => { + perform(); + }, 0); + } +} + +function perform() { + while (taskQueue.length) { + const task = peek(); + console.log("perform task: ", task); + task.execute(); + } + isLoopRunning = false; +} + +function push(task) { + taskQueue.push(task); + taskQueue.sort((a, b) => { + // 先比较过期时间,再比较 id + const diff = a.expirationTime - b.expirationTime; + return diff !== 0 ? diff : a.id - b.id; + }); +} + +function peek() { + return taskQueue.shift(); +} +``` + +```tsx preview +/** + * mini-scheduler-v2 + */ +const taskQueue = []; + +let id = 0; + +function schedule(execute: () => void, priority: number) { + // 根据优先级计算出任务的过期时间,并以过期时间作为排序依据 + // 即使在插入底优任务后,不断有高优任务插入,也能保证随着时间的推移,底优任务被排在第一位 + let timeout; + if (priority === 0) { + timeout = 0; + } else { + timeout = 100; + } + const expirationTime = Date.now() + timeout; + const task = { + id: id++, + execute, + expirationTime + }; + push(task); + console.log("schedule task: ", task); + schedulePerform(); +} + +// 标识 loop 是否在执行中 +let isLoopRunning = false; + +function schedulePerform() { + if (!isLoopRunning) { + isLoopRunning = true; + // 暂时用 setTimeout 做延迟处理 + setTimeout(() => { + perform(); + }, 0); + } +} + +function perform() { + while (taskQueue.length) { + const task = peek(); + console.log("perform task: ", task); + task.execute(); + } + isLoopRunning = false; +} + +function push(task) { + taskQueue.push(task); + taskQueue.sort((a, b) => { + // 先比较过期时间,再比较 id + const diff = a.expirationTime - b.expirationTime; + return diff !== 0 ? diff : a.id - b.id; + }); +} + +function peek() { + return taskQueue.shift(); +} +/** + * demo + */ +export default () => { + const onHighPriorityClick = () => { + const execute = () => { + const container = document.getElementById("mini-scheduler-v2"); + if (container) { + const node = document.createElement("div"); + node.innerText += Date.now(); + container.appendChild(node); + } + }; + + schedule(execute, /* 0 代表高优先级*/ 0); + schedule(execute, /* 0 代表高优先级*/ 0); + }; + + const onLowPriorityClick = () => { + const execute = () => { + const container = document.getElementById("mini-scheduler-v2"); + if (container) { + container?.classList.toggle("bg-sky-500"); + } + }; + + schedule(execute, /* 1 代表低优先级*/ 1); + }; + + const onMixPriorityClick = () => { + schedule(() => { + const container = document.getElementById("mini-scheduler-v2"); + if (container) { + const node = document.createElement("div"); + node.innerText += "低优"; + container.appendChild(node); + } + }, 1); + + schedule(() => { + const container = document.getElementById("mini-scheduler-v2"); + if (container) { + const node = document.createElement("div"); + node.innerText += "高优"; + container.appendChild(node); + } + }, 0); + }; + + return ( +
+
+ + + +
+
+
+ ); +}; +``` + +v2 版本相对核心链路更完善,但也只是达到了 Demo 演示的目的,距离业务可用标准还相差甚远。接下来,一起来看看 `Scheduler` 具体是怎么做的。 + +## Scheduler 流程概览 + +![](/d2/scheduler.svg) + +## 事件优先级 + +在 Scheduler 中,任务从高优先级到低优先级分别为:`ImmediatePriority`(立即执行)、`UserBlockingPriority`(用户阻塞级别)、`NormalPriority`(普通优先级)、`LowPriority`(低优先级)、`IdlePriority`(闲置)。 + +```ts title="react/packages/scheduler/src/SchedulerPriorities.js" +export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5; + +export const NoPriority = 0; +export const ImmediatePriority = 1; +export const UserBlockingPriority = 2; +export const NormalPriority = 3; +export const LowPriority = 4; +export const IdlePriority = 5; +``` + +## 小顶堆 + +## 源码解析 diff --git a/packages/rspress-site/docs/public/d2/mini-scheduler-v1.d2 b/packages/rspress-site/docs/public/d2/mini-scheduler-v1.d2 new file mode 100644 index 0000000..55d77da --- /dev/null +++ b/packages/rspress-site/docs/public/d2/mini-scheduler-v1.d2 @@ -0,0 +1,24 @@ +direction: right + +taskQueue: { + grid-columns: 3 + task1 + task2 + task3 +} + +schedule -> taskQueue + +perform: { + hasTask: 还存在任务 { + shape: diamond + } + + peek: peek\n取出优先级最高的任务 + + hasTask -> peek: yes + peek -> execute -> hasTask +} + +taskQueue -> perform.hasTask +perform.hasTask -> end: no diff --git a/packages/rspress-site/docs/public/d2/mini-scheduler-v1.svg b/packages/rspress-site/docs/public/d2/mini-scheduler-v1.svg new file mode 100644 index 0000000..22fc99e --- /dev/null +++ b/packages/rspress-site/docs/public/d2/mini-scheduler-v1.svg @@ -0,0 +1,128 @@ + + + + + + + + +taskQueuescheduleperformendtask1task2task3还存在任务peek取出优先级最高的任务execute yes no + + + + + + + + + + + + + + diff --git a/packages/rspress-site/docs/public/d2/mini-scheduler-v2.d2 b/packages/rspress-site/docs/public/d2/mini-scheduler-v2.d2 new file mode 100644 index 0000000..91963f7 --- /dev/null +++ b/packages/rspress-site/docs/public/d2/mini-scheduler-v2.d2 @@ -0,0 +1,26 @@ +direction: right + +taskQueue: { + grid-columns: 3 + task1 + task2 + task3 +} + +schedule -> taskQueue + +schedulePerform: schedulePerform\n(通过宏任务延迟调用 perform) + +perform: { + hasTask: 还存在任务 { + shape: diamond + } + + peek: peek\n取出优先级最高的任务 + + hasTask -> peek: yes + peek -> execute -> hasTask +} + +taskQueue -> schedulePerform -> perform.hasTask +perform.hasTask -> end: no diff --git a/packages/rspress-site/docs/public/d2/mini-scheduler-v2.svg b/packages/rspress-site/docs/public/d2/mini-scheduler-v2.svg new file mode 100644 index 0000000..78cc2cd --- /dev/null +++ b/packages/rspress-site/docs/public/d2/mini-scheduler-v2.svg @@ -0,0 +1,129 @@ + + + + + + + + +taskQueuescheduleschedulePerform(通过宏任务延迟调用 perform)performendtask1task2task3还存在任务peek取出优先级最高的任务execute yes no + + + + + + + + + + + + + + + diff --git a/packages/rspress-site/docs/public/d2/scheduler.d2 b/packages/rspress-site/docs/public/d2/scheduler.d2 new file mode 100644 index 0000000..f3b305d --- /dev/null +++ b/packages/rspress-site/docs/public/d2/scheduler.d2 @@ -0,0 +1,117 @@ +direction: down +*.style: { + border-radius: 8 +} +Queue: { + TimerQueue: TimerQueue(延迟任务队列)\n优先级按照 startTime 排序 { + timer1 -> timer2 -> timer3 + } + + TaskQueue: TaskQueue(立即执行任务队列)\n优先级按照 expirationTime 排序 { + task1 -> task2 -> task3 + } + + shouldDelay: 是否是延迟任务 { + shape: diamond + } + shouldDelay -> TimerQueue: yes + + shouldDelay -> TaskQueue: no +} + +Schedule: { + isFirstTimer: 当前任务是优先级最高的延迟任务\n且不存在立即执行的任务 { + shape: diamond + width: 360 + } + + requestHostTimeout: requestHostTimeout\n(使用 timeout 延迟,之后再次检查任务情况) + + handleTimeout: { + style: { + stroke-dash: 6 + } + advanceTimers: advanceTimers\n(将所有到期的延迟任务移动到TaskQueue中) + + hasDelayTask: TaskQueue存在任务 { + shape: diamond + } + + advanceTimers -> hasDelayTask + } + + schedulePerformWorkUntilDeadline: schedulePerformWorkUntilDeadline\n(通过宏任务延迟调用 performWorkUntilDeadline) + + handleTimeout.hasDelayTask -> requestHostTimeout: no + + handleTimeout.hasDelayTask -> requestHostCallback: yes + + isFirstTimer -> requestHostTimeout: yes + + requestHostTimeout -> handleTimeout + + requestHostCallback -> schedulePerformWorkUntilDeadline +} + +Execution: { + performWorkUntilDeadline -> flushWork -> workLoop.advanceTimers + + workLoop: { + style: { + stroke-dash: 6 + } + advanceTimers: advanceTimers\n(将所有到期的延迟任务移动到TaskQueue中) + + peek: peek\n(从 TaskQueue 中选择优先级最高的任务) + + shouldYield: 立即执行任务已经到期\n并且不需要让出主线程给浏览器 { + shape: diamond + width: 400 + } + + execute: 执行任务 + + finish: 当前任务执行完成 { + shape: diamond + width: 200 + } + + advanceTimers -> peek -> shouldYield + + shouldYield -> execute: yes + + execute -> finish + + finish -> advanceTimers: yes + } + + hasMore: 存在其他立即执行任务 { + shape: diamond + width: 300 + } + + hasMoreDelay: 存在其他延迟任务 { + shape: diamond + width: 300 + } + + workLoop.shouldYield -> hasMore: no + + hasMore -> hasMoreDelay: no +} + +push: push\n(根据优先级生成任务) + +scheduleCallback -> push -> Queue.shouldDelay + +Queue.TimerQueue -> Schedule.isFirstTimer + +Queue.TaskQueue -> Schedule.requestHostCallback + +Schedule.schedulePerformWorkUntilDeadline -> Execution.performWorkUntilDeadline + +Execution.workLoop.finish -> Schedule.schedulePerformWorkUntilDeadline: no +Execution.hasMore -> Schedule.schedulePerformWorkUntilDeadline: yes + +Execution.hasMoreDelay -> Schedule.requestHostTimeout: yes +Execution.hasMoreDelay -> end: no diff --git a/packages/rspress-site/docs/public/d2/scheduler.svg b/packages/rspress-site/docs/public/d2/scheduler.svg new file mode 100644 index 0000000..01bc876 --- /dev/null +++ b/packages/rspress-site/docs/public/d2/scheduler.svg @@ -0,0 +1,161 @@ + + + + + + + + +QueueScheduleExecutionpush(根据优先级生成任务)scheduleCallbackendTimerQueue(延迟任务队列)优先级按照 startTime 排序TaskQueue(立即执行任务队列)优先级按照 expirationTime 排序是否是延迟任务当前任务是优先级最高的延迟任务且不存在立即执行的任务requestHostTimeout(使用 timeout 延迟,之后再次检查任务情况)handleTimeoutschedulePerformWorkUntilDeadline(通过宏任务延迟调用 performWorkUntilDeadline)requestHostCallbackperformWorkUntilDeadlineflushWorkworkLoop存在其他立即执行任务存在其他延迟任务timer1timer2timer3task1task2task3advanceTimers(将所有到期的延迟任务移动到TaskQueue中)TaskQueue存在任务advanceTimers(将所有到期的延迟任务移动到TaskQueue中)peek(从 TaskQueue 中选择优先级最高的任务)立即执行任务已经到期并且不需要让出主线程给浏览器执行任务当前任务执行完成 yes no no yes yes yes yes no no no yes yes no + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/rspress-site/tailwind.config.js b/packages/rspress-site/tailwind.config.js index 3d6a5ea..a60abe2 100644 --- a/packages/rspress-site/tailwind.config.js +++ b/packages/rspress-site/tailwind.config.js @@ -1,5 +1,5 @@ module.exports = { - content: ["./components/**/*.tsx", "./docs/**/*.mdx"], + content: ["./src/**/*.tsx", "./docs/**/*.mdx"], theme: { extend: {}, },