Fiber 调度原理(Scheduler)学习笔记
一、requestIdleCallback 原理
- 核心作用
- requestIdleCallback 是浏览器提供的空闲期调度 API,其核心能力是在浏览器主线程空闲的时间段内执行回调任务,不会阻塞页面的关键渲染流程(布局、绘制、用户交互等)。
- 浏览器每帧(约 16.67ms,60fps)的执行流程为:处理用户事件 → 执行 JS → 布局(Layout) → 绘制(Paint),若某一帧完成所有核心工作后仍有剩余时间,该时间段即为「空闲期」,requestIdleCallback 注册的任务会在此期间执行。
- 关键特性
- 非高优先级:空闲期可被新的高优先级任务(如用户点击、滚动)抢占,未执行完的任务会在下一个空闲期继续
- 超时兜底:支持传入第二个参数 { timeout: 毫秒数 },若任务在超时时间内仍未被执行,浏览器会在主线程繁忙时强制执行,避免任务永久挂起;
- 空闲时间可控:回调函数会接收一个 IdleDeadline 参数,通过 deadline.timeRemaining() 可获取当前空闲期剩余时间,用于判断任务是否需要中断。
- 局限性(React 弃用原生 API 的原因)
- 兼容性差:IE 完全不支持,部分移动端浏览器支持度低;
- 触发频率低:浏览器在页面闲置时才会频繁触发,页面繁忙时可能几秒才触发一次,无法满足 React 高频调度需求;
- 精度不足:时间计算存在偏差,无法精准控制任务执行的时间切片。
总结:requestIdleCallback 是 React Scheduler 的设计灵感来源,但 React 基于其核心思想实现了自研的 Scheduler,解决了原生 API 的所有问题。
二、Scheduler 核心 —— 优先级设计
React Scheduler 的核心目标是按优先级调度任务,避免低优先级任务阻塞高优先级任务(如用户输入、点击比数据请求渲染优先级更高),其优先级体系是基于时间的过期策略,核心分为「优先级定义」和「调度规则」两部分。
- 优先级核心定义:过期时间(expirationTime)
Scheduler 不使用「字符串 / 数字等级」定义优先级(如 high/low、1-5),而是通过任务的过期时间(expirationTime) 表征优先级:
- 过期时间越短(任务越快过期),优先级越高;
- 过期时间越长(任务越晚过期),优先级越低;
- 已过期的任务(当前时间 ≥ 过期时间)会被立即执行,抢占所有未过期任务的执行权。
- 内置优先级等级(从高到低)
React 为常用场景预设了优先级,对应不同的过期时间(单位:ms),核心等级如下(实际源码中为常量定义):
| 优先级等级 | 过期时间 | 适用场景 |
|---|---|---|
| 同步优先级 | 0 | 紧急更新(如用户输入、点击) |
| 高优先级 | 250 | 动画、过渡效果 |
| 中优先级 | 5000 | 普通 UI 更新(如列表渲染) |
| 低优先级 | 10000 | 非紧急任务(如数据预加载) |
| 空闲优先级 | Infinity | 完全空闲时执行(如日志上报) |
- 核心调度规则
- 任务队列按过期时间升序排序(高优先级任务排在队首);
- 每次仅执行队首的高优先级任务,低优先级任务需等待高优先级任务执行完毕;
- 执行过程中若有新的更高优先级任务进入队列,立即中断当前任务,先执行新任务(「抢占式调度」核心);
- 所有任务均未过期时,按「时间切片」执行,避免阻塞主线程。
三、时间切片(Time Slicing)实现
时间切片是 Scheduler 最核心的实现,将长任务拆分为多个可中断的小任务,每个小任务执行时间不超过一个「切片时间」,剩余任务放到下一个切片执行,从而保证主线程不被长期阻塞,页面保持流畅。
- 时间切片的核心目标
突破 JS 单线程限制(无法真正并行),通过「分块执行 + 可中断」模拟并行效果,确保:
- 每个切片执行时间 ≤ 5ms(React 预设,远小于浏览器一帧 16.67ms);
- 浏览器有足够时间处理一帧的核心工作(布局、绘制、用户交互);
- 任务执行过程可被高优先级任务抢占,无卡顿。
- 核心实现原理(替代原生 requestIdleCallback)
React 自研了基于 requestAnimationFrame + MessageChannel 的空闲期检测机制,解决原生 API 缺陷,核心流程如下:
- 通过 requestAnimationFrame 获取每帧的开始时间,计算出当前帧的剩余可用时间(16.67ms - 已执行时间);
- 使用 MessageChannel 创建微任务级别的调度通道,将任务放到 MessageChannel 的回调中执行(优先级高于宏任务,避免任务延迟);
- 执行任务前做「剩余时间检测」:通过 performance.now() 计算当前切片已执行时间,若超过预设切片时间(5ms),立即中断任务;
- 中断后将剩余任务重新加入任务队列,等待下一个切片时间继续执行;
- 若执行过程中检测到高优先级任务或浏览器无空闲时间,立即中断,优先处理主线程核心工作。
- 核心实现要点
- 可中断:任务执行过程中无全局锁,通过「剩余时间检测」主动中断,而非被动等待;
- 无阻塞:每个切片执行时间极短,浏览器有足够时间处理一帧的所有核心工作;
- 抢占式:中断后高优先级任务可插队,保证用户交互等紧急操作的响应速度;
- 兼容性强:基于浏览器通用 API(requestAnimationFrame、MessageChannel),无兼容性短板。
四、模拟实现:时间切片(Time Slicing)
- 实现目标
- 模拟 React Scheduler 核心的时间切片能力,实现:
- 长任务自动拆分为小任务,每个小任务执行时间 ≤ 5ms;
- 任务执行过程可被中断,剩余任务自动续跑;
- 不阻塞主线程,页面可正常响应用户交互。
- 代码实现
/** * 模拟 React Scheduler 时间切片实现 * 核心:分块执行长任务 + 剩余时间检测 + 可中断 */classTimeSlicingScheduler{constructor(){this.taskQueue=[];// 任务队列this.isRunning=false;// 是否正在执行任务,避免重复调度this.timeSlice=5;// 切片时间,默认5ms(同React)}/** * 添加任务到队列 * @param {Function} task - 要执行的任务(需是可分块的迭代器函数) * @param {number} priority - 优先级(数字越小,优先级越高) */addTask(task,priority=10){this.taskQueue.push({task:this.wrapTask(task),// 包装为迭代器,支持分块执行priority,startTime:performance.now(),});// 按优先级升序排序(高优先级在前)this.taskQueue.sort((a,b)=>a.priority-b.priority);// 启动调度this.schedule();}/** * 将普通函数包装为迭代器,支持分块执行(核心:可中断) * @param {Function} task - 原始长任务 * @returns {Generator} 迭代器对象 */wrapTask(task){returnfunction*(){yieldtask();// 分块执行,支持中断后续跑}();}/** * 核心调度方法:时间切片执行任务 */schedule(){// 若已有任务在执行,直接返回(避免重复执行)if(this.isRunning)return;this.isRunning=true;// 启动任务执行:使用 requestAnimationFrame 对齐浏览器帧constframeCallback=(timestamp)=>{// 执行任务,直到切片时间用尽或任务队列为空consthasMoreTasks=this.executeTasks(timestamp);if(hasMoreTasks){// 还有剩余任务,继续调度下一帧requestAnimationFrame(frameCallback);}else{// 任务执行完毕,重置状态this.isRunning=false;}};requestAnimationFrame(frameCallback);}/** * 执行任务核心逻辑:剩余时间检测 + 分块执行 * @param {number} startTime - 当前帧开始时间 * @returns {boolean} 是否还有剩余任务 */executeTasks(startTime){letcurrentTask=this.taskQueue[0];if(!currentTask)returnfalse;const{task}=currentTask;letshouldContinue=true;// 循环执行,直到切片时间用尽或任务执行完毕while(shouldContinue&¤tTask){// 检测剩余时间:当前时间 - 帧开始时间 > 切片时间 → 中断constelapsedTime=performance.now()-startTime;if(elapsedTime>this.timeSlice){shouldContinue=false;// 切片时间用尽,中断break;}// 执行当前任务的一个小切片(迭代器next)constresult=task.next();// 若任务执行完毕(迭代器done),从队列中移除if(result.done){this.taskQueue.shift();}// 更新当前任务(队列首元素)currentTask=this.taskQueue[0];}// 返回是否还有剩余任务(队列非空 或 当前任务未执行完)returnthis.taskQueue.length>0||(currentTask&&!shouldContinue);}}// ---------------------- 测试用例 ----------------------// 1. 初始化调度器constscheduler=newTimeSlicingScheduler();// 2. 模拟一个长任务:循环10000次,打印计数(正常执行会阻塞主线程)functionlongTask(){letcount=0;return()=>{// 每次切片执行100次,分100块执行(避免单次阻塞)for(leti=0;i<100;i++){count++;if(count%1000===0){console.log(`长任务执行中:${count}/10000`);}}// 任务未执行完时,继续返回执行if(count<10000){returnfalse;}console.log("长任务执行完毕!");returntrue;};}// 3. 添加长任务到调度器(优先级10)scheduler.addTask(longTask(),10);// 4. 添加高优先级任务(优先级1,会插队执行)scheduler.addTask(()=>{console.log("【高优先级任务】执行:用户点击事件处理");returntrue;},1);// 测试:页面点击事件(验证不阻塞)document.addEventListener("click",()=>{console.log("页面点击响应:无卡顿,主线程未被阻塞!");});- 代码核心说明
- 迭代器包装(wrapTask):将长任务包装为 Generator 迭代器,通过 task.next() 实现分块执行,这是任务可中断的核心(中断后下次执行从上次的 next() 继续);
- 优先级排序:添加任务时按优先级升序排序,保证高优先级任务始终在队首执行;
- 帧对齐(requestAnimationFrame):让任务执行与浏览器帧同步,避免浪费空闲时间;
- 剩余时间检测:通过 performance.now() 计算已执行时间,超过 5ms 立即中断,保证主线程空闲;
- 无阻塞验证:测试用例中添加了页面点击事件,执行长任务时点击页面可正常响应,无卡顿。
- 运行效果
- 高优先级任务先执行:打印「【高优先级任务】执行:用户点击事件处理」;
- 长任务分块执行:每次打印「长任务执行中:1000/10000」「2000/10000」…,每块执行时间 ≤5ms;
- 页面交互正常:执行过程中点击页面,立即打印「页面点击响应:无卡顿,主线程未被阻塞!」;
- 任务执行完毕:最后打印「长任务执行完毕!」,无任何阻塞。
五、Scheduler 与 Fiber 架构的关联
- Scheduler 是 React Fiber 架构的调度层核心,为 Fiber 树的构建和更新提供底层支持:
- Fiber 树的调和(Reconciliation)过程被拆分为多个小任务,由 Scheduler 按时间切片执行;
- Fiber 节点的优先级与 Scheduler 任务优先级一致,保证高优先级的 Fiber 更新(如用户交互)可抢占低优先级更新;
- Scheduler 的抢占式调度是 Fiber 「可中断调和」的基础,调和过程中可随时中断,跳过高优先级任务的调和;
- 调和过程中若时间切片用尽或有高优先级任务,Scheduler 会中断执行,将剩余工作交给下一个切片,保证页面流畅。
六、核心知识点总结
- requestIdleCallback 是 Scheduler 的设计灵感,但其兼容性和触发频率问题导致 React 自研调度机制;
- Scheduler 优先级基于过期时间,过期时间越短优先级越高,支持抢占式调度;
- 时间切片的核心是长任务分块 + 可中断 + 剩余时间检测,通过 requestAnimationFrame + MessageChannel 实现;
- 时间切片的关键是主动中断,而非被动等待,保证主线程不被阻塞;
- Scheduler 是 Fiber 架构的底层支撑,为 Fiber 调和提供优先级调度和时间切片能力。