本篇react教程是基于React18版本,对应的代码在gitee上。
一、理解fiber
定义:fiber是react里的一种数据结构,以链表(类循环链表)的形式存在。
下面是一个fiber的基本完整结构,先来简单理解每个属性的含义:
- containerInfo: 存放的是顶层真实dom节点
- current: 当前的Fiber节点,可以理解为当前屏幕展示的fiber节点,和alternate(workInProgress)形成双缓存结构
- alternate: 和current的结构是一致的,前一个版本的fiber节点信息。可以理解为正在处理的fiber节点。这有助于在进行差异比较和更新时,能够回退到之前的状态。
- tag: 这个属性表示Fiber节点的类型。FunctionComponent = 0 函数组件、ClassComponent = 1 类组件、IndeterminateComponent = 2 未知状态组件、HostRoot = 3 容器根节点、HostComponent = 5 原生节点 span、HostText = 6 纯文件节点
- key: 用于标识Fiber节点的key值。
- type: 表示Fiber节点的类型,可以是组件的构造函数或者是HTML标签字符串。FunctionComponent、button
- stateNode: 已经渲染完成的真实dom节点数据。
- return: 指向父Fiber节点。
- child: 指向当前Fiber节点的子节点。
- childLanes: 这是一个位字段,用于追踪子节点的更新优先级,react会根据优先级去处理fiber节点。
- deletions: 一个列表,用于存储已经被删除的子节点。
- flags: 这是一个位字段,用于存储节点的状态信息,比如是否需要更新,是否有状态等。NoFlags 无副作用、Placement 插入、Update 更新、ChildDeletion 有子节点需要被删除
- index: 节点在父节点子节点列表中的索引。
- memoizedProps: 存储节点的已传递(屏幕渲染的)属性。函数组件是props、原生节点是{ style="" }
- pendingProps: 存储即将应用到节点上的新属性。
- sibling: 指向当前节点的兄弟节点。
- subtreeFlags: 这是一个位字段,用于存储子树的状态信息,类似于flags。
- updateQueue: 这是一个队列,用于存储待处理的更新。(前面的结构就是这个队列,节点的更新都存储在这里执行)
下面是fiber的结构图:
关于current和altername(workInProgress)的进一步理解
我们可以将其分为两部分,一部分是current当前屏幕渲染的节点,一部分altername(workInProgress)正在处理的节点,他们是双缓存结构,可以认为current是altername(workInProgress)的复制品。
其优点是可以双向执行,altername(workInProgress)在构建fiber树,current一边渲染到页面上,可以提升执行效率,加快dom节点的替换和更新。
二、理解fiber的updateQueue
更新队列updateQueue的链表形式,在react多处用到,必须理解。
为了方便理解updateQueue链表,下面是一个非常简单的链表某一部分模拟代码。
function initialUpdateQueue(fiber) {
fiber.pending = null;
}
function createUpdate() {
return {};
}
function enqueueUpdate(fiber, update) {
const pending = fiber.pending;
if (pending === null) {
update.next = update;
} else {
//如果更新队列不为空的话,取出第一个更新
update.next = pending.next;
//然后让原来队列的最后一个的next指向新的next
pending.next = update;
}
fiber.pending = update;
}
let fiber = {};
initialUpdateQueue(fiber);
let update1 = createUpdate();
update1.value = "第1个添加";
enqueueUpdate(fiber, update1);
let update2 = createUpdate();
update2.value = "第2个添加";
enqueueUpdate(fiber, update2);
let update3 = createUpdate();
update3.value= "第3个添加";
enqueueUpdate(fiber, update3);
console.log(fiber);
看一下输出的updateQueue结构,第一层是最后添加的”第3个添加“,第二层是第一个添加的”第1个添加“,依次这样会一直循环下去的结构。
这样的结构有一个非常好的特点就是使用fiber.pending.value就能拿到最后一个片段,使用fiber.pending.next.value就能拿到第一个片段。
我们还要引入一个循环链表的操作,这样可以循环处理链表的每一个片段。在react中就可以对每个片段进行处理。后面会介绍到是如何分片处理的。
processUpdateQueue方法会对fiber链进行循环并处理。
function initialUpdateQueue(fiber) {
fiber.pending = null;
}
function createUpdate() {
return {};
}
function enqueueUpdate(fiber, update) {
const pending = fiber.pending;
if (pending === null) {
update.next = update;
} else {
//如果更新队列不为空的话,取出第一个更新
update.next = pending.next;
//然后让原来队列的最后一个的next指向新的next
pending.next = update;
}
fiber.pending = update;
}
function processUpdateQueue(fiber) {
const pending = fiber.pending;
if (pending !== null) {
fiber.pending = null;
//最后一个更新
const lastPendingUpdate = pending;
const firstPendingUpdate = lastPendingUpdate.next;
//把环状链接剪开,形成单链机构
lastPendingUpdate.next = null;
let newState = fiber.memoizedState;
let update = firstPendingUpdate;
while (update) {
newState = Object.assign({}, newState, {value: update.value});
update = update.next;
}
fiber.memoizedState = newState;
}
}
let fiber = {memoizedState: {}};
initialUpdateQueue(fiber);
let update1 = createUpdate();
update1.value = "第1个添加";
enqueueUpdate(fiber, update1);
let update2 = createUpdate();
update2.value = "第2个添加";
enqueueUpdate(fiber, update2);
let update3 = createUpdate();
update3.value= "第3个添加";
enqueueUpdate(fiber, update3);
processUpdateQueue(fiber);
console.log(fiber);
三、无fiber处理vdom过程
react15之前没有fiber,虚拟dom的渲染是不能停止的,导致阻塞问题。有了fiber之后,因为fiber可以暂停执行,使用了分片处理,后文会提到。
下面是不使用fiber的情况下渲染虚拟dom的过程:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script>
//虚拟DOM
let vdom = {
"type": "div",
"key": "A1",
"props": {
"id": "A1",
"children": [
{
"type": "div",
"key": "B1",
"props": {
"id": "B1",
"children": [
{
"type": "div",
"key": "C1",
"props": { "id": "C1" },
},
{
"type": "div",
"key": "C2",
"props": { "id": "C2" },
}
]
},
},
{
"type": "div",
"key": "B2",
"props": { "id": "B2" },
}
]
},
}
//以前我们直接把vdom渲染成了真实DOM
function render(vdom, container) {
//根据虚拟DOM生成真实DOM
let dom = document.createElement(vdom.type);
//把除children以外的属性拷贝到真实DOM上
Object.keys(vdom.props).filter(key => key !== 'children').forEach(key => {
dom[key] = vdom.props[key];
});
//代码执到此,想中断,无法实现断点续执行
//把此虚拟DOM的子节点,也渲染到父节点真实DOM上
if (Array.isArray(vdom.props.children)) {
vdom.props.children.forEach(child => render(child, dom));
}
container.appendChild(dom);
}
render(vdom, document.getElementById('root'));
</script>
</body>
</html>
因为这是一个递归遍历的过程,没有办法随时进行停止。而fiber不同,构建fiber树的过程,或者说渲染的过程变成可中断,可暂停和恢复的过程,fiber递归特点就是,逐级深度优先遍历,到达底点,又一步步返回到顶点的过程。
四、fiber的处理过程
下面是fiber工作过程的代码:
//1.把虚拟DOM构建成fiber树
let A1 = { type: 'div', props: { id: 'A1' } };
let B1 = { type: 'div', props: { id: 'B1' }, return: A1 };
let B2 = { type: 'div', props: { id: 'B2' }, return: A1 };
let C1 = { type: 'div', props: { id: 'C1' }, return: B1 };
let C2 = { type: 'div', props: { id: 'C2' }, return: B1 };
//A1的第一个子节点B1
A1.child = B1;
//B1的弟弟是B2
B1.sibling = B2;
//B1的第一个子节点C1
B1.child = C1;
//C1的弟弟是C2
C1.sibling = C2;
//下一个工作单元
let nextUnitOfWork = null;
const hasTimeRemaining = () => Math.floor(Math.random() * 10) % 2 == 0;
//render工作循环
function workLoop(){
//工作循环每一个处理一个fiber,处理完以后可以暂停
//如果有下一个任务并且有剩余的时间的话,执行下一个工作单元,也就是一个fiber
while(nextUnitOfWork){
//while(nextUnitOfWork && hasTimeRemaining()){ // 先注释掉 hasTimeRemaining,这是分片处理中的剩余时间
//执行一个任务并返回下一个任务
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
//render阶段结束
console.log('render阶段结束')
}
function performUnitOfWork(fiber){//A1
let child = beginWork(fiber);
//如果执行完A1之后,会返回A1的第一个子节点
if(child){
return child;
}
//如果没有子节点
while(fiber){//如果没有子节点说明当前节点已经完成渲染工作
completeUnitOfWork(fiber);//可以结束此fiber的渲染了
if(fiber.sibling){//如果它有弟弟就返回弟弟
return fiber.sibling;
}
fiber = fiber.return;
}
}
function beginWork(fiber){
console.log('beginWork', fiber.props.id);
return fiber.child;//B1
}
function completeUnitOfWork(fiber){
console.log('completeUnitOfWork', fiber.props.id);
}
nextUnitOfWork = A1;
workLoop();
1、fiber处理节点是深度优先遍历的过程
2、从A1顶部开始查找,找到最深处执行,找到B1,找到C1没有儿子了
3、执行C1,查找其兄弟执行C2,回到父亲执行B1,查找其兄弟执行B2,回到父亲执行A1
下面是输出直接过程:
下面是节点树:
这也是react处理fiber的简化执行流程,唯一的区别就是,真实的源码执行过程调用到处理的方法比较复杂而已。
五、fiber暂停-分片处理
性能瓶颈
- JS 任务执行时间过长
- 浏览器刷新频率为 60Hz,大概 16.6 毫秒渲染一次,而 JS 线程和渲染线程是互斥的,所以如果 JS 线程执行任务时间超过 16.6ms 的话,就会导致掉帧,导致卡顿,解决方案就是 React 利用空闲的时间进行更新,不影响渲染进行的渲染
- 把一个耗时任务切分成一个个小任务,分布在每一帧里的方式就叫时间切片
屏幕刷新率
- 目前大多数设备的屏幕刷新率为 60 次/秒
- 浏览器渲染动画或页面的每一帧的速率也需要跟设备屏幕的刷新率保持一致
- 页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿
- 每个帧的预算时间是 16.66 毫秒 (1 秒/60)
- 1s 60 帧,所以每一帧分到的时间是 1000/60 ≈ 16 ms,所以我们书写代码时力求不让一帧的工作量超过 16ms
帧
- 每个帧的开头包括样式计算、布局和绘制
- JavaScript 执行 Javascript 引擎和页面渲染引擎在同一个渲染线程,GUI 渲染和 Javascript 执行两者是互斥的
- 如果某个任务执行时间过长,浏览器会推迟渲染
requestIdleCallback
- 我们希望快速响应用户,让用户觉得够快,不能阻塞用户的交互
- requestIdleCallback 使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应
- 正常帧任务完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务
下面是分片代码例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<script>
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d; );
}
const works = [
() => {
console.log("第1个任务开始");
sleep(20); //sleep(20);
console.log("第1个任务结束");
},
() => {
console.log("第2个任务开始");
sleep(20); //sleep(20);
console.log("第2个任务结束");
},
() => {
console.log("第3个任务开始");
sleep(20); //sleep(20);
console.log("第3个任务结束");
},
];
requestIdleCallback(workLoop);
function workLoop(deadline) {
console.log("本帧剩余时间", parseInt(deadline.timeRemaining()));
while (deadline.timeRemaining() > 1 && works.length > 0) {
performUnitOfWork();
}
if (works.length > 0) {
console.log(
`只剩下${parseInt(
deadline.timeRemaining()
)}ms,时间片到了等待下次空闲时间的调度`
);
requestIdleCallback(workLoop);
}
}
function performUnitOfWork() {
works.shift()();
}
</script>
</body>
</html>
分片注意
- 分成的单个小任务不会中断,如果是2000ms也要执行完才释放空间,因为已经是最小单位
- 释放空间之后,如果有优先级更高的事件会先执行,不如点击click操作
- react内部使用的不是requestIdleCallback,而是自己实现的,一般分片是5ms一个,所以不会出现第一个问题
- 合作调度,如果有空闲时间就执行react这边的任务,没空闲的意思是高优先级任务在执行
fiber优点
- 我们可以通过某些调度策略合理分配 CPU 资源,从而提高用户的响应速度
- 通过 Fiber 架构,让自己的调和过程变成可被中断。 适时地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互
下面是fiber执行过程的调度流程:
六、总结:
- 理解fiber结构的属性值含义
- 理解更新队列updateQueue
- 没有fiber的react处理虚拟dom的方法
- fiber处理虚拟dom的过程
- react的分片处理
下一篇,fiber更新队列updateQueue(代码篇)