937 words
5 minutes
React调度器(Scheduler)的手写实现
2025-03-13 08:46:36
2025-03-13 23:11:11

基础版本实现#

首先,让我们实现一个最简单的调度器版本。这里我没有使用官方的scheduleCallback命名,而是使用doCallback,因为在这个简单版本中,传入的回调函数会立即执行:

let currentTask = null; export function doCallback(callback) { currentTask = callback; workLoop(); } function workLoop() { currentTask(); }

这个实现非常简单,但存在一个明显问题:由于JavaScript是单线程的,如果任务执行时间过长,会阻塞主线程,导致页面无法响应用户交互,造成卡顿,影响用户体验。

改进版:基于时间切片的调度器#

下面是基于requestIdleCallback改进的版本,它能够在浏览器空闲时执行任务:

const taskQueue = [] as Callback[]; function pop(array: Callback[]) { array.shift(); } function push(array: Callback[], callback: Callback) { array.push(callback); } function peek(array: Callback[]) { return array[0] as Callback | undefined; } type Callback = () => void; export function scheduleCallback(callback: Callback) { push(taskQueue, callback); requestHostCallback(); } function requestHostCallback() { requestIdleCallback(deadline => { workLoop(deadline); }); } function workLoop(deadline: IdleDeadline) { let currentTask = peek(taskQueue); while (currentTask && (deadline.timeRemaining() > 0 || deadline.didTimeout)) { // 先出栈,避免死循环 pop(taskQueue); currentTask(); currentTask = peek(taskQueue); } }

演示1:阻塞问题展示#

演示1源码
import { Button } from '@/content/components/Button'; import { scheduleCallback } from './scheduler'; const clickHandle = () => { const startTime = performance.now(); scheduleCallback(() => { console.log(startTime, '最外层'); scheduleCallback(() => { console.log(startTime, '第二层'); scheduleCallback(() => { console.log(startTime, '第三层'); }); }); }); scheduleCallback(() => { const start = performance.now(); while (performance.now() - start < 1000) {} console.log(startTime, '最外层2'); }); }; export const Component001 = () => { return <Button onClick={clickHandle}>点击</Button>; };

点击上面的按钮后,可以观察到后面的任务都被1秒的耗时任务阻塞了。这正是我们需要解决的问题。

引入优先级的调度器#

为了解决上述问题,我们需要引入任务优先级的概念。在React的调度器中,priorityLevel用于计算任务的超时时间。

超时时间计算#

以下是从React源码中提取的计算超时时间的逻辑:

import { userBlockingPriorityTimeout, lowPriorityTimeout, normalPriorityTimeout, } from './SchedulerFeatureFlags'; import { IdlePriority, ImmediatePriority, LowPriority, NormalPriority, UserBlockingPriority, type PriorityLevel, } from './SchedulerPriorities'; // Max 31 bit integer. The max integer size in V8 for 32-bit systems. // Math.pow(2, 30) - 1 // 0b111111111111111111111111111111 var maxSigned31BitInt = 1073741823; export function calculateTimeout(priorityLevel: PriorityLevel) { let timeout = -1; switch (priorityLevel) { case ImmediatePriority: // Times out immediately timeout = -1; break; case UserBlockingPriority: // Eventually times out timeout = userBlockingPriorityTimeout; break; case IdlePriority: // Never times out timeout = maxSigned31BitInt; break; case LowPriority: // Eventually times out timeout = lowPriorityTimeout; break; case NormalPriority: default: // Eventually times out timeout = normalPriorityTimeout; break; } return timeout; }

因此,我们的scheduleCallback函数签名需要修改为:

function scheduleCallback(priorityLevel: PriorityLevel, callback: Callback): void

获取当前时间#

既然有超时时间,我们需要一个获取当前时间的函数:

export function getCurrentTime() { return performance.now(); }

任务队列设计#

任务队列不能只存储简单的回调函数,我们需要设计一个完整的Task类型:

export interface Task { callback: Callback | null; priorityLevel: PriorityLevel; startTime: number; // 用户发起任务的时间 expirationTime: number; // startTime + 由priorityLevel计算得到的超时时间 sortIndex: number; // 优先队列中排序的依据,等于expirationTime }

完整的调度器实现#

优先级调度器实现
import { calculateTimeout } from '../utils/calculateTimeout'; import { getCurrentTime } from '../utils/getCurrentTime'; import { peek, pop, push } from '../utils/minHeap'; import type { PriorityLevel } from '../utils/SchedulerPriorities'; import type { Task } from './task'; const taskQueue = [] as Task[]; type Callback = () => void; let isHostCallbackScheduled = false; export function scheduleCallback( priorityLevel: PriorityLevel, callback: Callback, ) { const currentTime = getCurrentTime(); const startTime = currentTime; const expirationTime = startTime + calculateTimeout(priorityLevel); const sortIndex = expirationTime; const task: Task = { callback, priorityLevel, startTime, expirationTime, sortIndex, }; push(taskQueue, task); if (!isHostCallbackScheduled) { isHostCallbackScheduled = true; requestHostCallback(); } } function requestHostCallback() { requestIdleCallback(deadline => { isHostCallbackScheduled = false; workLoop(deadline); }); } function workLoop(deadline: IdleDeadline) { let currentTask = peek(taskQueue); while (currentTask && (deadline.timeRemaining() > 0 || deadline.didTimeout)) { // 先出栈,避免死循环 pop(taskQueue); currentTask.callback(); currentTask = peek(taskQueue); } }

演示2:优先级调度效果#

演示2源码
import { Button } from '@/content/components/Button'; import { scheduleCallback } from './scheduler'; import { ImmediatePriority, UserBlockingPriority, } from '../utils/SchedulerPriorities'; function getCurrentTime() { return performance.now().toFixed(1); } export const clickHandle = () => { const log = createLog(); scheduleCallback(ImmediatePriority, function cb1() { log(getCurrentTime(), '最外层'); scheduleCallback(ImmediatePriority, function cb2() { log(getCurrentTime(), '第二层'); scheduleCallback(ImmediatePriority, function cb3() { log(getCurrentTime(), '第三层'); }); }); }); scheduleCallback(UserBlockingPriority, function cb1_1() { const start = performance.now(); while (performance.now() - start < 1000) {} log(getCurrentTime(), '最外层2'); }); }; export const Component002 = () => { return <Button onClick={clickHandle}>点击</Button>; }; function createLog() { const startTime = getCurrentTime(); const color = getRandomColor(); return function log(...args: string[]) { console.log(`%c${[startTime, ...args].join(' ')}`, `color: ${color}`); }; } // 生成随机颜色的函数 function getRandomColor() { // 生成随机的RGB值 const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); const b = Math.floor(Math.random() * 256); // 返回十六进制颜色代码 return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; }

现在,单击一次按钮,可以看到最外层的延迟任务不再阻塞其他两个低优先级任务了:

单击一次效果

但是,如果快速点击两次,我们会发现第一次点击的延迟任务仍然阻塞了第二次点击的高优先级任务:

快速单击两次效果

验证问题#

为了确认这不是React导致的问题,我们在一次点击中调用两次scheduleCallback

验证源码
import { Button } from '@/content/components/Button'; import { clickHandle } from './component'; export const Component002_2 = () => { return ( <Button onClick={() => { clickHandle(); clickHandle(); }} > 模拟单击两次 </Button> ); };

验证结果

好了,迷茫中,不知道如何下一步仿写,或者说非得要往官方实现上靠的理由是什么??? 目前没有足够的需求,所以手写源码告一段落,后续有想法再更新


思考与问题#

通过手写实现React调度器的核心部分,我们对其工作原理有了初步了解。但仍有一些问题值得思考:

  1. 全局优先级状态currentPriorityLevel这个全局状态的设置有什么作用?

  2. 优先级环境:React源码中的unstable_runWithPriority函数设计引发了一个疑问:

    function unstable_runWithPriority<T>( priorityLevel: PriorityLevel, eventHandler: () => T, ): T { // ...省略部分代码 var previousPriorityLevel = currentPriorityLevel; currentPriorityLevel = priorityLevel; try { return eventHandler(); } finally { currentPriorityLevel = previousPriorityLevel; } }

    既然eventHandler是立即执行的,为什么还需要保存和恢复previousPriorityLevel

    参考:

  3. 任务检查:为什么需要检查currentTask是否为堆顶元素?

    const continuationCallback = callback(didUserCallbackTimeout); // ... if (currentTask === peek(taskQueue)) { pop(taskQueue); }

    答:这是因为回调函数在执行过程中可能会再次调用unstable_scheduleCallback,这个调用会同步地创建新任务并更新堆顶元素。


React调度器(Scheduler)的手写实现
https://0bipinnata0.my/posts/react/handwritten/scheduler/
Author
0bipinnata0
Published at
2025-03-13 08:46:36