xdm,又要到饭了,又更新代码了!
总结一下上一篇完成的内容,
- 实现了mini diff 算法
有兴趣的可以点这里查看diff 算法的迷你版实现
这篇将会实现一个fiber 的迷你版本。这里,简单介绍一下fiber 的功能和解决的问题。
Fiber 将整个渲染过程(包括协调和提交阶段)拆分为多个小任务,每个小任务代表渲染工作的一部分。这些小任务可以在每个时间切片内执行,确保主线程不会被长时间占用。这个优势在复杂组件 / 资源消耗重的场景优势特别明显。它的出现也特别针对这样的场景提出的解决方法。
那么根据上面的定义,我们需要完成
- 渲染过程拆分多个小任务,每一个小任务渲染一部分工作
- 任务需要可以按照等级的高低,调整任务的执行顺序(优先级)
- 需要配合帧或者刷新率来调度,如果当前的frame 没有空闲,则需要等待下一个frame 完成剩余任务。
- 调度器需要记录当前任务的运行状态, 可以中断/恢复上一次剩余的任务。
Mini Fiber 的实现
1.1 优先级常量定义
js
const PRIORITY = {
IMMEDIATE: 1, // 最高优先级:立即执行
USER_BLOCKING: 2, // 用户交互任务,尽快执行
NORMAL: 3, // 普通任务
LOW: 4, // 低优先级任务
IDLE: 5 // 闲置时才执行的任务
};
- 调度器核心数据结构
js
let taskQueue = []; // 存储待执行的任务
let currentTask = null; // 当前正在执行的任务
let isPerformingTasks = false; // 标记任务是否在执行
- 调度函数
js
function schedule(task, priority = PRIORITY.NORMAL) {
taskQueue.push({ task, priority, progress: 0 }); // 添加任务到队列
taskQueue.sort((a, b) => a.priority - b.priority); // 按优先级排序
requestHostCallback(); // 请求执行任务
}
function requestHostCallback() {
if (!isPerformingTasks) {
requestIdleCallback(performTasks); // 在浏览器空闲时执行任务
}
}
- 执行任务的函数
js
function performTasks(deadline) {
isPerformingTasks = true;
while (taskQueue.length > 0 && deadline.timeRemaining() > 0) {
if (currentTask === null) {
currentTask = taskQueue.shift(); // 获取下一个任务
}
const { taskFn, priority, progress } = currentTask;
const chunkDuration = PRIORITY[priority] <= PRIORITY.USER_BLOCKING ? 10 : 5; // 高优先级任务可以获取更多的分片完成任务。
const taskStartTime = performance.now();
let isTaskCompleted = false;
// 执行任务的一部分
while (performance.now() - taskStartTime < chunkDuration) {
isTaskCompleted = taskFn(currentTask.progress); // 执行任务并更新进度
currentTask.progress++;
if (isTaskCompleted) break; // 任务完成
}
if (isTaskCompleted) {
currentTask = null; // 任务完成,重置当前任务
} else {
// 任务未完成,等待下一次执行
break;
}
}
// 如果还有任务未完成,则继续调度
if (taskQueue.length > 0 || currentTask !== null) {
requestIdleCallback(performTasks);
} else {
isPerformingTasks = false; // 所有任务已完成
}
}
- 修改
setState
使用调度器
js
function setStateWithScheduler(newState) {
schedule(() => {
setState(newState); // 假设已有 setState 实现
}, PRIORITY.USER_BLOCKING); // 用户交互任务优先级
}
- 处理长任务的示例
js
function longTask(progress) {
console.log(`Progress: ${progress.current} / ${progress.total}`);
// 模拟执行任务的一部分
progress.current++;
// 判断任务是否完成
return progress.current >= progress.total;
}
// 初始化任务进度
const longTaskProgress = { current: 0, total: 10 };
// 调度长任务
schedule(() => longTask(longTaskProgress), PRIORITY.NORMAL);
解释与优化
1. 任务拆分与进度管理
在 executeTask
函数中,每个任务维护自己的 progress
状态。这允许任务在每次执行时只完成一小部分,并在下次空闲时间继续执行剩余部分。这样,长任务不会阻塞主线程,可以被拆分为多个小任务块逐步完成。(这个对应实现了我们一开始设定需要完成的第1条)
2. 优先级排序
任务队列在每次添加新任务后会根据优先级重新排序,确保高优先级任务优先执行。这模仿了 React Fiber 的优先级调度机制,保证用户交互任务能够快速响应。(这个对应实现了我们一开始设定需要完成的第2条)
3. 时间切片与任务执行
通过为不同优先级的任务分配不同的 chunkDuration
,高优先级任务可以在较长的时间切片内完成更多工作,而低优先级任务则分配较短的时间切片。这种策略帮助保持应用的响应性。(这个对应实现了我们一开始设定需要完成的第3条)
4. 避免无限递归
使用 isPerformingTasks
标志位来确保在任务执行过程中不会重复调用 requestIdleCallback
,避免陷入无限递归调用,确保调度器的稳定性。
React 是否使用 requestIdleCallback
?
requestIdleCallback
的使用
- React Fiber 本身并没有直接依赖
requestIdleCallback
来实现任务调度。 - React 的内部调度器(如 scheduler 包)更倾向于使用一种更通用和灵活的方法 ,结合
requestIdleCallback
及其替代方案,以确保在各种浏览器环境中都能高效运行。
兼容性问题
-
浏览器支持不一致:
requestIdleCallback
在某些浏览器中支持不佳或尚未完全支持(如早期的 Safari 版本)。
-
可靠性与精确性:
requestIdleCallback
的调用时间和执行环境在不同浏览器中可能有所差异,影响任务调度的一致性。
React 的解决方案
为了应对 requestIdleCallback
的兼容性问题,React 的调度器通常采用以下策略:
-
条件使用
requestIdleCallback
:- 当浏览器支持
requestIdleCallback
时,优先使用它来执行低优先级任务。
- 当浏览器支持
-
提供回退方案:
- 在
requestIdleCallback
不可用的环境中,使用setTimeout
或requestAnimationFrame
作为替代。
- 在
-
灵活的调度逻辑:
- 根据当前浏览器环境动态选择最佳的调度方法,确保任务调度的高效性和一致性。
这一篇实现了一个迷你版本的fiber,也大致实现了我们一开始设定需要完成的4个点,
- 渲染过程拆分多个小任务,每一个小任务渲染一部分工作
- 任务需要可以按照等级的高低,调整任务的执行顺序(优先级)
- 需要配合帧或者刷新率来调度,如果当前的frame 没有空闲,则需要等待下一个frame 完成剩余任务。
- 调度器需要记录当前任务的运行状态, 可以中断/恢复上一次剩余的任务。
我们下一篇会引入fiber 树的概念。这一篇实现了一个简单版本fiber,让你对fiber 有一个基本的了解,大致解决了什么问题,以及使用什么方法解决问题。
下一篇,我们将继续完成实现迷你版本的fiber 架构以帮助我们增强其原理的理解。
如果这样的长度/强度你觉得可以接受,觉得有帮助,可以继续阅读下一篇,实现一个 Mini React:核心功能详解 - fiber 的迷你版实现(2)。
如果文章对你有帮助,请点个赞支持一下!
啥也不是,散会。