面试题:是否了解过 React 的整体渲染流程?里面主要有哪些阶段?
现代前端框架都可以总结为一个公式:
UI = f(state)
上面的公式还可以进行一个拆分:
- 根据自变量(state)的变化计算出 UI 的变化
- 根据 UI 变化执行具体的宿主环境的 API
对应的公式:
const state = reconcile(update); // 通过 reconciler 计算出最新的状态
const UI = commit(state); // 根据上一步计算出来的 state 渲染出 UI
对应到 React 里面就两大阶段:
- render 阶段:调合虚拟 DOM,计算出最终要渲染出来的虚拟 DOM
- commit 阶段:根据上一步计算出来的虚拟 DOM,渲染具体的 UI
每个阶段对应不同的组件:
- 调度器(Scheduer):调度任务,为任务排序优先级,让优先级高的任务先进入到 Reconciler
- 协调器(Reconciler):生成 Fiber 对象,收集副作用,找出哪些节点发生了变化,打上不同的 flags,著名的 diff 算法也是在这个组件中执行的。
- 渲染器(Renderer):根据协调器计算出来的虚拟 DOM 同步渲染节点到视图上。
接下来我们来看一个例子:
export default () => {
const [count, updateCount] = useState(0);
return (
<ul>
<button onClick={() => updateCount(count + 1)}>乘以{count}</button>
<li>{1 * count}</li>
<li>{2 * count}</li>
<li>{3 * count}</li>
</ul>
);
}
当用户点击按钮时,首先是由 Scheduler 进行任务的协调,render 阶段的工作流程是可以随时被以下原因中断:
- 有其他更高优先级的任务需要执行
- 当前的 time slice 没有剩余的时间
- 发生了其他错误
注意上面 render 阶段的工作是在内存里面进行的,不会更新宿主环境 UI,因此这个阶段即使工作流程反复被中断,用户也不会看到“更新不完整的UI”。
当 Scheduler 调度完成后,将任务交给 Reconciler,Reconciler 就需要计算出新的 UI,最后就由 Renderer 同步进行渲染更新操作。
如下图所示:
调度器
在 React v16 版本之前,采用的是 Stack 架构,所有任务只能同步进行,无法被打断,这就导致浏览器可能会出现丢帧的现象,表现出卡顿。React 为了解决这个问题,从 v16 版本开始从架构上面进行了两大更新:
- 引入 Fiber
- 新增了 Scheduler
Scheduler 在浏览器的原生 API 中实际上是有类似的实现的,这个 API 就是 requestIdleCallback
MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
虽然每一帧绘制的时间约为 16.66ms,但是如果屏幕没有刷新,那么浏览器会安排长度为 50ms 左右的空闲时间。
为什么是50ms?
根据研究报告表明,用户操作之后,100ms以内的响应给用户的感觉都是瞬间发生,也就是说不会感受到延迟感,因此将空闲时间设置为 50,浏览器依然还剩下 50ms 可以处理用户的操作响应,不会让用户感到延迟。
虽然浏览器有类似的 API,但是 React 团队并没有使用该 API,因为该 API 存在兼容性问题。因此 React 团队自己实现了一套这样的机制,这个就是调度器 Scheduler。
后期 React 团队打算单独发行这个 Scheduler,这意味着调度器不仅仅只能在 React 中使用,凡是有涉及到任务调度需求的项目都可以使用 Scheduler。
我们利用requestIdleCallback来模拟一下Scheduler的工作方式
// 通过 requestIdleCallback 去模拟 Scheduler 的工作流程
function delay(duration) {
const start = Date.now();
while (Date.now() - start < duration) {}
}
const taskList = []; // 存放任务的队列
// 推入任务
for (let i = 1; i <= 10; i++) {
taskList.push(() => {
delay(10);
console.log(`执行任务${i}`);
});
}
// 接下来我们想要执行任务,每一帧渲染完毕后有剩余时间
// 如果时间充足,我们就执行任务
// 如果时间不充足,那么就在下一帧渲染后再接着执行
function callback(IdleDeadline) {
// 执行任务
console.log(
"当前帧绘制完毕后所剩余的时间:",
IdleDeadline.timeRemaining()
);
while (IdleDeadline.timeRemaining() > 0 && taskList.length) {
// 还有剩余时间,并且任务列表还有任务
const task = taskList.shift();
task();
}
// 退出上面的 while 后,有一种情况是当前帧的时间不够了,但是任务列表中还有剩余任务
if (taskList.length) {
// 那么我们就在下一帧空闲时间再继续执行任务
window.requestIdleCallback(callback);
}
}
window.requestIdleCallback(callback);
总结:
React 的 Scheduler 是一个负责调度任务的小型库,它的主要目的是为了优化用户界面的响应性和性能。Scheduler 的工作原理和优先级机制可以概括如下:
1. 任务调度
Scheduler 允许开发者安排任务在不同的时间点执行,而不是立即执行。这有助于避免长时间占用主线程,导致用户界面卡顿。
2. 优先级机制
Scheduler 通过为不同的任务分配不同的优先级来实现任务调度。优先级的划分通常基于任务的紧急程度和对用户体验的影响。React 中常见的优先级类型包括:
- Immediate:立即执行的任务,如用户输入。
- User-blocking:阻塞用户操作的任务,如点击按钮后的响应。
- Normal:普通任务,如数据更新。
- Low:低优先级任务,如背景数据同步。
3. 时间分片(Time Slicing)
Scheduler 通过时间分片技术,将长任务拆分成多个小任务,并在每个时间片中执行一部分任务。这样可以在执行长任务的同时,保持用户界面的响应性。
4. 工作原理
Scheduler 的工作流程大致如下:
- 任务入队:开发者通过
Scheduler.scheduleTask
方法将任务加入调度队列,并指定任务的优先级。 - 任务排序:Scheduler 根据任务的优先级对队列中的任务进行排序,确保高优先级任务优先执行。
- 任务执行:Scheduler 在合适的时间点(如浏览器空闲时)从队列中取出最高优先级的任务执行。
- 时间分片:如果任务执行时间过长,Scheduler 会暂停当前任务,并在下一个时间片继续执行。
示例代码
以下是一个简单的示例,展示如何在 React 中使用 Scheduler:
import { unstable_scheduleCallback as scheduleCallback } from 'scheduler';
const task = () => {
console.log('Task executed');
};
// 调度一个低优先级的任务
scheduleCallback(Scheduler.unstable_LowPriority, task);
// 调度一个高优先级的任务
scheduleCallback(Scheduler.unstable_ImmediatePriority, task);
注意事项
- 实验性 API:Scheduler 中的 API 是实验性的,可能会在未来的版本中发生变化。
- 浏览器兼容性:Scheduler 主要针对现代浏览器设计,可能在老旧浏览器中表现不佳。
总结
React 的 Scheduler 通过优先级机制和时间分片技术,有效地管理任务的执行顺序和时间,从而提升应用的响应性和性能。理解 Scheduler 的工作原理,有助于开发者编写更高效、更流畅的 React 应用。
希望这些信息对你有所帮助!如果有更多问题,欢迎继续提问。
协调器
协调器是 render 阶段的第二阶段工作,类组件或者函数组件本身就是在这个阶段被调用的。
根据 Scheduler 调度结果的不同,协调器起点可能是不同的
- performSyncWorkOnRoot(同步更新流程)
- performConcurrentWorkOnRoot(并发更新流程)
// performSyncWorkOnRoot 会执行该方法
function workLoopSync(){
while(workInProgress !== null){
performUnitOfWork(workInProgress)
}
}
// performConcurrentWorkOnRoot 会执行该方法
function workLoopConcurrent(){
while(workInProgress !== null && !shouldYield()){
performUnitOfWork(workInProgress)
}
}
新的架构使用 Fiber(对象)来描述 DOM 结构,最终需要形成一颗 Fiber tree,这不过这棵树是通过链表的形式串联在一起的。
workInProgress 代表的是当前的 FiberNode。
performUnitOfWork 方法会创建下一个 FiberNode,并且还会将已创建的 FiberNode 连接起来(child、return、sibling),从而形成一个链表结构的 Fiber tree。
如果 workInProgress 为 null,说明已经没有下一个 FiberNode,也就是说明整颗 Fiber tree 树已经构建完毕。
上面两个方法唯一的区别就是是否调用了 shouldYield方法,该方法表明了是否可以中断。
performUnitOfWork在创建下一个 FiberNode 的时候,整体上的工作流程可以分为两大块:
- 递阶段
- 归阶段
递阶段
递阶段会从 HostRootFiber 开始向下以深度优先的原则进行遍历,遍历到的每一个 FiberNode 执行 beginWork 方法。该方法会根据传入的 FiberNode 创建下一级的 FiberNode,此时可能存在两种情况:
- 下一级只有一个元素,beginWork 方法会创建对应的 FiberNode,并于 workInProgress 连接
<ul>
<li></li>
</ul>
这里就会创建 li 对应的 FiberNode,做出如下的连接:
LiFiber.return = UlFiber;
- 下一级有多个元素,这时 beginWork 方法会依次创建所有的子 FiberNode 并且通过 sibling 连接到一起,每个子 FiberNode 也会和 workInProgress 连接
<ul>
<li></li>
<li></li>
<li></li>
</ul>
此时会创建 3 个 li 对应的 FiberNode,连接情况如下:
// 所有的子 Fiber 依次连接
Li0Fiber.sibling = Li1Fiber;
Li1Fiber.sibling = Li2Fiber;
// 子 Fiber 还需要和父 Fiber 连接
Li0Fiber.return = UlFiber;
Li1Fiber.return = UlFiber;
Li2Fiber.return = UlFiber;
由于采用的是深度优先的原则,因此无法再往下走的时候,会进入到归阶段。
归阶段
归阶段会调用 completeWork 方法来处理 FiberNode,做一些副作用的收集。
当某个 FiberNode 执行完了 completeWork 方法后,如果存在兄弟元素,就会进入到兄弟元素的递阶段,如果不存在兄弟元素,就会进入父 FiberNode 的归阶段。
function performUnitOfWork(fiberNode){
// 省略 beginWork
if(fiberNode.child){
performUnitOfWork(fiberNode.child);
}
// 省略 CompleteWork
if(fiberNode.sibling){
performUnitOfWork(fiberNode.sibling);
}
}
最后我们来看一张图:
渲染器
Renderer 工作的阶段被称之为 commit 阶段。该阶段会将各种副作用 commit 到宿主环境的 UI 中。
相较于之前的 render 阶段可以被打断,commit 阶段一旦开始就会同步执行直到完成渲染工作。
整个渲染器渲染过程中可以分为三个子阶段:
- BeforeMutation 阶段
- Mutation 阶段
- Layout 阶段
真题解答
题目:是否了解过 React 的整体渲染流程?里面主要有哪些阶段?
参考答案:
React 整体的渲染流程可以分为两大阶段,分别是 render 阶段和 commit 阶段。
render 阶段里面会经由调度器和协调器处理,此过程是在内存中运行,是异步可中断的。
commit 阶段会由渲染器进行处理,根据副作用进行 UI 的更新,此过程是同步不可中断的,否则会造成 UI 和数据显示不一致。
调度器
调度器的主要工作就是调度任务,让所有的任务有优先级的概念,这样的话紧急的任务可以优先执行。Scheduler 实际上在浏览器的 API 中是有原生实现的,这个 API 叫做 requestIdleCallback,但是由于兼容性问题,React 放弃了使用这个 API,而是自己实现了一套这样的机制,并且后期会把 Scheduler 这个包单独的进行发布,变成一个独立的包。这就意味 Scheduler 不仅仅是只能在 React 中使用,后面如果有其他的项目涉及到了任务调度的需求,都可以使用这个 Scheduler。
协调器
协调器是 Render 的第二阶段工作。该阶段会采用深度优先的原则遍历并且创建一个一个的 FiberNode,并将其串联在一起,在遍历时分为了“递”与“归”两个阶段,其中在“递”阶段会执行 beginWork 方法,该方法会根据传入的 FiberNode 创建下一级 FiberNode。而“归”阶段则会执行 CompleteWork 方法,做一些副作用的收集
渲染器
渲染器的工作主要就是将各种副作用(flags 表示)commit 到宿主环境的 UI 中。整个阶段可以分为三个子阶段,分别是 BeforeMutation 阶段、Mutation 阶段和 Layout 阶段。注意那个图片上的调度新的更新就是指下一个交互重头开始了,重新开始调度器协调器渲染器流程了。