React源码系列之任务调度机制Scheduler

一、Scheduler概述

1、Scheduler

Scheduler是一个独立的包,它可以独自承担起任务调度的职责;它与React组成了一个任务调度系统;

React是任务产生的地方;Scheduler是任务的调度者;

Scheduler和React相互配合的模式可以让React的任务执行具备异步可中断的特点;

2、Scheduler的优先级及时间片

(1)优先级

Scheduler拥有属于自己的优先级,并且会根据这些优先级设置超时时间timeout;

按照任务自身的紧急程度排序,保证高优先级的任务优先执行;

JavaScript 复制代码
export const NoPriority = 0; // 没有优先级 
export const ImmediatePriority = 1; // 立即执行的优先级,级别最高 
export const UserBlockingPriority = 2; // 用户阻塞级别的优先级 
export const NormalPriority = 3; // 正常优先级 
export const LowPriority = 4; // 较低优先级 
export const IdlePriority = 5; // 优先级最低,表示任务闲置

(2)timeout设置

Scheduler会根据startTime与timeout设置任务的过期时间expirationTime;

JavaScript 复制代码
var maxSigned31BitInt = 1073741823; 
var IMMEDIATE_PRIORITY_TIMEOUT = -1; // 立即执行的任务 
var USER_BLOCKING_PRIORITY_TIMEOUT = 250; // 用户操作的任务 
var NORMAL_PRIORITY_TIMEOUT = 5000; 
var LOW_PRIORITY_TIMEOUT = 10000; 
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt; // 空闲任务

(3)时间切片

时间片规定的是单个任务在这一帧内最大的执行时间,任务执行时间一旦超出时间片的限制,就会被中断;可以保证页面不会因为任务连续执行导致时间过长产生卡顿;

具体做法是:

将VDOM的执行过程拆分成一个个独立的宏任务,将每个宏任务的执行时间限制在一定范围内,初始为5ms;即:将一个会造成掉帧的长任务拆解为多个不会掉帧的短宏任务,以减少掉帧的可能性,这一技术被称为时间切片。

3、Scheduler调度任务的模式

在浏览器中,JS引擎线程与UI渲染线程是互斥的,Scheduler不会一直占用线程去执行任务,而是执行一会,中断一下,继续执行,避免一直占用资源执行任务,解决用户操作时页面卡顿的问题;

Scheduler在执行多个任务时,会按照任务的优先级去执行,保证高优先级的任务优先执行;

4、Scheduler的职责

Scheduler主要负责管理任务、安排任务的执行;不过Scheduler只是去做任务的中断和恢复,任务是否完成依赖于React;

Scheduler执行任务具备的特点:根据时间片去中止任务,并判断任务是否完成,未完成则继续调用任务函数。

Scheduler的核心任务就是回调,回调函数由react-reconciler提供,回调函数名称:performConcurrentWorkOnRoot

concurrent模式才支持时间分片;React v18默认开启了该模式;

5、Scheduler中任务的管理

(1)任务管理队列

  • timerQueue:存放未过期的任务
  • taskQueue:存放已过期的任务

(2)如何判断任务是否过期?

用任务的开始时间startTime和当前时间currentTime对比:

  • 开始时间 > 当前时间,说明未过期,放入到timerQueue队列;
  • 开始时间 <= 当前时间,说明已过期,放入到taskQueue队列;

(3)不同队列中的任务如何排序?

依据任务的紧急程度排序,只不过依据的参数不一样;

  • timerQueue:
    • startTime:开始时间
      • 开始时间越早,说明任务越早开始,开始时间小的排在前边;
      • 任务进来时,默认是当前时间,如果进入调度时,传入延迟时间,开始时间 = 当前时间 + 延迟时间;
  • taskQueue:
    • expirationTime:任务过期时间
      • 过期时间越早,任务越紧急,排在前边;
      • 过期时间根据任务优先级计算得出,优先级越高,过期时间越早;【任务优先级是计算任务过期时间的重要依据,事关过期任务在taskQueue中的排序】

(4)任务的处理

  • taskQueue队列
    • 会立即调度一个函数去循环taskQueue,挨个执行里边的任务;
  • timerQueue队列
    • 该任务不会立即执行,需要调用handleTimeout检查该队列中的第一个任务是否过期
      • 过期,任务从timerQueue中删除,添加到taskQueue中;
      • 不过期,继续检查等待第一个任务过期,循环以上操作;

6、Scheduler中的角色分配

  • 调度入口:scheduleCallback
    • 该函数的调用参数:scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root), );
    • performConcurrentWorkOnRoot函数就是我们需要根据优先级分类的任务
  • 开始调度的函数:requestHostCallback
  • 执行任务的函数:workloop(关键逻辑所在)「功能:循环taskQueue执行任务 和 任务状态的判断」
  • 调度者:
    • 非浏览器环境是setTimeout;
    • 浏览器环境是:MessageChannel实例的port.postMessage;
  • 执行者:调用时期的任务执行函数;
    • 执行者:performWorkUntilDeadline
    • 作用:按照时间片的限制去中断任务,并通知调度者再次调度一个新的执行者去继续执行任务;

7、Scheduler工作流程小结

Scheduler管理着taskQueue与timerQueue两个队列,他会定期调用advanceTimers将timerQueue中的过期任务放到taskQueue中,然后让调度者通知执行者循环taskQueue执行掉每一个任务。

执行者控制着每个任务的执行,一旦某个任务的执行时间超出时间片的限制,就会被中断,然后当前的执行者退场,退场之前会通知调度者再去调度一个新的执行者继续完成这个任务,新的执行者在执行任务时依旧会根据时间片中断任务,然后退场,重复这一过程,直到当前这个任务彻底执行完成后,将任务从taskQueue中出队。

taskQueue中的每一个任务都这样被处理,最终完成所有任务。

二、任务的中断及恢复

1、单个任务的中断及恢复:

在循环taskQueue执行每一个任务时,如果某个任务的执行时间过长,达到了时间片限制的时间,该任务必须中断,以便于让位给更重要的事情,像浏览器的绘制等,等事情完成,再恢复任务的执行。

单个任务的完成只能依赖任务函数的返回值去判断;

2、任务是否完成的判断

开始调度后,调度者调度执行者去执行任务;如果执行者判断callback返回值为一个function,说明未完成,会将function再次赋值给callback;

当前任务不会从taskQueue中删除,因为任务还没有执行结束,下次循环时,取到这个任务,继续执行;

3、任务的中止

currentTask表示当前正在执行的任务,它中止的判断条件是:任务并未过期,但已经没有剩余时间了,需要让出执行权给主线程;

由于它只是被中止,currentTask的取值不是null,return一个true表示任务还没执行结束(这是恢复任务的关键),否则说明全部的任务已经执行完成,taskQueue队列被清空,return一个false表示终止本次调度。

flushWork实际上是scheduledHostCallback,当performWorkUntilDeadline检测到scheduledHostCallback的返回值为false,就会停止调度;


JavaScript 复制代码
// 任务的中止判断条件:任务并未过期,但是已经没有剩余时间;或者需要让出执行权给主线程(时间片的限制)
if (
    currentTask.expirationTime > currentTime && 
    ( !hasTimeRemaining || shouldYieldToHost() )
) {
    // 退出本次循环,后边的逻辑无法执行 
    // 这是任务中断的关键 
    /* ----- 任务只是被中止,还没执行结束 ------ */ 
    break; 
}

三、Scheduler源码分析

1、unstable_scheduleCallback

(1)函数作用

  • 处理任务过期时间;
  • 根据过期时间给任务分类;
  • 进入任务调度流程;

(2)任务过期时间计算

JavaScript 复制代码
// 获取当前时间:它是计算任务开始时间、过期时间和判断任务是否过期的依据 
var currentTime = getCurrentTime(); 
// 计算任务开始时间 
var startTime; 
if (typeof options === 'object' && options !== null) { 
    var delay = options.delay; 
    if (typeof delay === 'number' && delay > 0) { 
        startTime = currentTime + delay; // 传入延迟时间 
    } else { 
        startTime = currentTime;
    } 
} else { 
    startTime = currentTime; 
} 
// 根据优先级计算一个延迟时间 
var timeout; 
switch (priotiryLevel) { 
    case ImmediatePriority: 
        timeout = IMMEDIATE_PRIORITY_TIMEOUT;
        break; 
    case UserBlockingPriority: 
        timeout = USER_BLOCKING_PRIORITY_TIMEOUT; 
        break; 
    case IdlePriority: 
        timeout = IDLE_PRIORITY_TIMEOUT; 
        break; 
    case LowPriority: 
        timeout = LOW_PRIORITY_TIMEOUT; 
        break; 
    case NormalPriority: 
    default: 
        timeout = NORMAL_PRIORITY_TIMEOUT; 
        break; 
} 
// 计算任务过期时间 
var expirationTime = startTime + timeout;

(3)Scheduler中的任务结构

JavaScript 复制代码
var newTask = { 
    id: taskIdCounter++, callback, // 任务函数:真正的任务函数,外部传入【执行结果会影响到任务完成状态的判断】 
    priorityLevel, // 任务优先级:参与计算任务过期时间 
    startTime, // 任务开始时间:影响timerQueue中的排序 
    expirationTime, // 任务过期时间:影响taskQueue中的排序 
    sortIndex: -1 // 在小顶堆中排序的依据:在区分好任务是过期还是非过期之后,sortIndex会被赋值为expirationTime或startTime 
};

(4)任务分组:根据是否过期【startTime > currentTime】

JavaScript 复制代码
if (startTime > currentTime) { // 任务未过期,以开始时间作为timerQueue排序的依据 
    newTask.sortIndex = startTime; 
    push(timerQueue, newTask); // 任务未过期,将newTask放入timerQueue 
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) { 
        /** 
        * 如果taskQueue中没有任务,且当前任务是timerQueue中排名最靠前的那一个 
        * 需要检查timerQueue中有没有需要放到taskQueue中的任务, 
        * 这一步通过调用requestHostTimeout实现 
        */ 
        if (isHostTimeoutScheduled) { // 即将调度一个requestHostTimeout。如果已经调度,则取消掉 
            cancelHostTimeout(); 
        } else { 
            isHostTimeoutScheduled = true; 
        } 
        // 调用requestHostTimeout实现任务的转移,开启调度 
        requestHostTimeout(handleTimeout, startTime - currentTime); 
    } 
} else { 
    // 任务已经过期,以过期时间作为taskQueue排序的依据 
    newTask.sortIndex = expirationTime; 
    push(taskQueue, newTask); // 将任务添加到taskQueue队列中 
    // 开始执行任务,使用flushWork去执行taskQueue 
    if (!isHostCallbackScheduled && !isPerformingWork) { 
        isHostCallbackScheduled = true; // 进入任务调度流程 
        // 任务的执行者本质上调用的就是flushWork 
        // flushWork:真正执行任务的函数,会循环taskQueue 
        requestHostCallback(); 
    } 
}

2、任务的调度

(1)requestHostCallback

进入任务调度流程的函数,接收一个参数:flushWork;

flushWork:任务的执行者本质上调用的就是flushWork,它是真正执行任务的函数;

  • 实现:该函数的实现区分浏览器环境和非浏览器环境;
    • 非浏览器环境不存在帧的概念,也就不会有时间片,可以使用setTimeout实现;
    • 浏览器环境下,任务的执行会有时间片的限制,使用MessageChannel实现;
JavaScript 复制代码
function requestHostCallback( // 该处的callback实际上就是传入的flushWork函数 
    callback: (hasTimeRemaining: boolean, initialTime: number) => boolean 
) { 
    scheduledHostCallback = callback; 
    if (!isMessageLoopRunning) { 
        isMessageLoopRunning = true; 
        schedulePerformWorkUntilDeadline(); 
    } 
}

(2)flushWork

JavaScript 复制代码
function flushWork(hasTimeRemaining: boolean, initialTime: number) { 
    isHostCallbackScheduled = false; 
    if (isHostTimeoutScheduled) { 
        isHostTimeoutScheduled = false; 
        cancelHostTimeout(); 
    } 
    isPerformingWork = true;
    const previousPriorityLevel = currentPriorityLevel; 
    // 循环 
    try { 
        return workLoop(hasTimeRemaining, initialTime); 
    } finally { 
        currentTask = null; 
        currentPriorityLevel = previousPriorityLevel; 
        isPerformingWork = false; 
    } 
}

(3)workLoop

该函数是任务执行的核心内容:实现任务的中断与恢复;

该函数的执行结果会被flushWork函数return出去;

JavaScript 复制代码
// 做了什么:循环taskQueue执行任务 任务状态的判断 
// 单个任务完成状态的判断:workLoop依赖任务函数的返回值去判断任务是否完成 
// 如任务返回值为函数,说明任务尚未完成,需要继续调用任务函数,否则任务完成 
function workLoop(hasTimeRemaining, initialTime) { 
    let currentTime = initialTime; // 开始执行前,检查timerQueue中的过期任务;并放入taskQueue中 
    advanceTimers(currentTime); 
    currentTask = peek(taskQueue); // 获取taskQueue中第一个要执行(最紧急)的任务 
    // 循环taskQueue,执行任务
    while (
        currentTask !== null && 
        !( enableSchedulerDebugging && isSchedulerPaused )
    ) { 
        // 任务的中止判断条件:任务并未过期,但是已经没有剩余时间;或者需要让出执行权给主线程(时间片的限制) 
        if (
            currentTask.expirationTime > currentTime 
            && ( !hasTimeRemaining || shouldYieldToHost() )
        ) { 
            // 退出本次循环,后边的逻辑无法执行 
            // 这是任务中断的关键
            /* ----- 任务只是被中止,还没执行结束 ------ */ 
            break; 
        }
        /* -----------------------执行任务------------------------- */ 
        const callback = currentTask.callback; // 获取任务的执行函数 
        if (typeof callback === 'function') { 
            // 如果callback是函数类型,表示当前有任务,可以执行 
            currentTask.callback = null; 
            // 获取任务的优先级 
            currentPriorityLevel = currentTask.priorityLevel; 
            // 判断任务是否过期 
            const didUserCallbackTimeout = currentTask.expirationTime <= currentTime; 
            // 获取任务函数的执行结果 
            const continuationCallback = callback(didUserCallbackTimeout); 
            currentTime = getCurrentTime(); 
            if (typeof continuationCallback === 'function') { 
                // 判断continuationCallback是否为函数,为函数,则将函数作为当前任务新的回调; 
                currentTask.callback = continuationCallback; 
                // 检查timerQueue中的过期任务,并放入taskQueue 
                advanceTimers(currentTime); 
                // 恢复任务的关键 
                return true; 
            } else { 
                if (currentTask === peek(taskQueue)) { 
                    pop(taskQueue);
                } 
                advanceTimers(currentTime); 
            } 
        } else { 
            // callback为null,任务出队 
            // 取消调度的关键是:将当前任务的callback设置为null; 
            // 因为:在workLoop中,callback为null就会被移出taskQueue,当前任务就不再被执行 
            pop(taskQueue);
        }
        // 从taskQueue中继续获取任务,如果上一个任务未完成,该任务不会从taskQueue中删除 
        // 获取的currentTask还是该任务,继续执行该任务 
        currentTask = peek(taskQueue); 
    } 
    // return的结果会作为performWorkUntilDeadline中判断是否还需要再次发起调取的依据 
    if (currentTask !== null) { 
        return true; 
    } else { 
        // 任务完成,去timerQueue中找优先级最高的任务调度requestHostTimeout
        // 目的:将其放入taskQueue中等待调度 
        const firstTimer = peek(timerQueue); 
        if (firstTimer !== null) {
            requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); 
        } 
        // 让外部终止本次调度 
        return false;
        } 
    }

(4)advanceTimers

JavaScript 复制代码
function advanceTimers(currentTime: number) { 
    // timerQueue队列中的任务不会执行,只是会每隔一段时间判断其是否过期 
    let timer = peek(timerQueue); // 获取timerQueue中优先级最高的任务 
    while(timer !== null) {
        if (timer.callback === null) { 
            // callback:真正要执行的任务
            pop(timerQueue);
        } else if (timer.startTime <= currentTime) { 
            // 任务开始时间小于当前时间,表示任务已经过期 
            pop(timerQueue); // 从timerQueue中删除该任务 
            timer.sortIndex = timer.expirationTime; // 设置排序依据
            push(taskQueue, timer); // 将任务添加到taskQueue队列中,等待执行
        } else { 
            return; 
        }
        timer = peek(timerQueue);
    }
}

(5)requestHostTimeout

JavaScript 复制代码
function requestHostTimeout( 
    callback: (currentTime: number) => void,
    ms: number 
) { 
    taskTimeoutID = localSetTimeout(() => { 
        callback(getCurrentTime());
    }, ms); 
}

(6)handleTimeout

  • 作用:检查timerQueue队列中的过期任务,并且添加到taskQueue中并执行;
  • 实现:
    • 调用advanceTimers,检查timerQueue队列中的过期任务,添加到taskQueue中;
    • 检查是否已经开始调度,若未调度,则检查taskQueue中是否已经有任务
      • 如果有,且空闲,立即开始调度,并执行任务;
      • 如果没有,且空闲,调用requestHostTime检查是否有过期任务
JavaScript 复制代码
function handleTimeout(currentTime: number) { 
    isHostTimeoutScheduled = false; 
    advanceTimers(currentTime); 
    if (!isHostCallbackScheduled) { 
        if (peek(taskQueue) !== null) { 
            isHostCallbackScheduled = true; 
            requestHostCallback(flushWork); 
        } else { 
            const firstTimer = peek(timerQueue);
            if (firstTimer !== null) { 
                    requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime); 
            } 
        }
    }
}

(7)performWorkUntilDeadline

JavaScript 复制代码
// 任务的执行:执行者 
// 按照时间片的限制去中断任务,并通知调度者再次调度一个新的执行者去继续任务 
const performWorkUntilDeadline = () => { 
    if (scheduledHostCallback !== null) { 
        const currentTime = getCurrentTime(); 
        startTime = currentTime; 
        // 表示任务是否还有剩余时间,与时间片一起限制任务的执行;
        // 如果没有时间,或者任务执行时间超出时间片限制,就中断任务 
        // postMessage的执行很靠前,中断任务就只看时间片 
        const hasTimeRemaining = true; 
        let hasMoreWork = true; 
        try { 
            // scheduledHostCallback:执行任务 
            // 任务因为时间片被打断,返回true;表示还有任务执行,调度者会再调度一个执行者继续执行任务 
            hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); 
        } finally { 
            if (hasMoreWork) { 
                // 有任务,调度者调度执行者,继续完成任务 
                schedulePerformWorkUntilDeadline();
            } else { 
                // 没有任务,则停止调度
                isMessageLoopRunning = false;
                scheduledHostCallback = null;
            }
        }
    } else { 
        isMessageLoopRunning = false; 
    }
    needPaint = false; 
}

(8)时间片的实现

MessageChannel在浏览器事件循环中属于宏任务,在调度中心属于异步调度;

JavaScript 复制代码
// 时间分片浏览器实现 
const channel = new MessageChannel(); 
const port = channel.port2; 
channel.port1.onmessage = performWorkUntilDeadline;
let schedulePerformWorkUntilDeadline = () => { 
    port.postMessage(null); 
};

(9)unstable_cancelCallback

JavaScript 复制代码
// 取消任务调度 
// 在workLoop中,callback设置为null,就会被移出taskQueue,那么当前任务就不再被执行; 
// 它只是取消当前任务的执行,while循环还会继续执行下一个任务 
function unstable_cancelCallback(task) { 
    task.callback = null; 
}
相关推荐
Patrick_Wilson17 小时前
从 React 到 Flutter:写给前端的一张跨端知识地图
前端·flutter·react.js
张元清1 天前
React useDropZone Hook:搭建一个文件拖放区(2026)
javascript·react.js
无人生还1 天前
从 Vue3 到 React · 快速上手系列第 5 篇:事件处理与表单(受控组件 vs v-model)
前端·vue.js·react.js
光影少年1 天前
RN Bridge 原理
前端·react native·react.js
moMo1 天前
用 React 写一个 TodoList
react native·react.js
To_OC2 天前
写了三遍 Todo List,我终于搞懂了 React 父子组件到底怎么通信
前端·javascript·react.js
kyriewen2 天前
别再乱用useEffect了——你写的10个里有8个不该存在
前端·javascript·react.js
zandy10112 天前
衡石 Agentic BI的ReAct 推理框架在 Agentic BI 中的工程化实践
前端·javascript·react.js
朝阳392 天前
react19【系列实用教程】JS 项目升级为 typescript
javascript·react.js·typescript
labixiong2 天前
React Compiler 用 Rust 重写了,编译提速 10 倍——手写 useMemo 的日子到头了
react.js·rust