React17系列(4)-初识Fiber与RIC
前言
此章介绍下Fiber的一些基础知识,算是对Fiber的一个入门,所以不会涉及到太多源码。同时,建议在大家如果在阅读源码时遇到了瓶颈,觉得无法进行下去的时候,可以适当放弃,先去找一个大神的mini-react,先对react这个框架有个整体了解,再回过头来读源码也未尝不可。这里推荐下我最近阅读的一篇文章,逐步引导我们完成一个mini版的react,不过文章是纯英文的,日后我也会写一个系列记录下此文章:Build your own react
ReactElement (jsx)/ Fiber / DOM的关系
首先通过这三者的关系认识下Fiber这个陌生的单词
ReactElement
之前的文章有提到过,我们书写的jsx文件最终都被React.createElement(...)的方式,创建出来与之对应的ReactElement对象
Fiber
fiber就是一个对象,通过ReactElement对象进行创建,每个dom节点都有其对应的fiber对象,多个fiber就构成了一颗Fiber树,可以理解为这颗大的树记录了页面要渲染的所有的DOM节点。(注意我这里的大小写,并不是写错了,一颗Fiber树中包含了许多fiber对象)
DOM
dom节点就不解释了,文档对象模型
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>
如上这段代码对应的Fiber树如下图:(parent: 父节点; child: 子节点: sibling: 兄弟节点)
为什么要引入Fiber
看了以上三者的关系,即使未接触过Fiber也会有个大概的理解。接下来介绍为什么react抛弃了之前的架构,采用了现在的Fiber架构。
React15
react15的构架分为两层:
-
Reconciler(协调器): 进行diff运算等,每当组件更新时,其将会做如下操作:
- jsx转为VDOM
- 此次VDOM与上次更新的VDOM对比
- diff算法找出差异
-
Renderer(渲染器): 将组件渲染到页面上
在Reconciler阶段,中途是不能被打断的(Stack Reconciler),直至栈空。整个过程的js计算,会一直占据浏览器主线程,页面就没办法得到及时的更新,当组件过于庞大时,就有可能出现掉帧的现象。
Now
针对上面的问题,Fiber架构产生了,主要针对如下两个问题进行解决
- reconciler阶段能否被打断暂停
- dom diff算法时,如何在一帧内不阻塞浏览器的渲染
React源码中实现过于复杂,但万变不离其宗,接下来介绍一个重要的API,react官方也是使用此api的思想来解决了如上问题。
浏览器一帧内会干什么以及RequestIdleCallback的启示
浏览器一帧内的任务
我们现在看到的网页都是浏览器一帧一帧绘制出来的,一秒绘制越多帧,画面就会越细腻。目前大多浏览器是60Hz(60帧/s,打游戏的各位可能会为帧数有些多的了解),每一帧绘制的时间大概也就是16.67ms左右,如上图可以知道在这一帧内的时间浏览器做了什么:
- 处理输入事件
- 执行事件回调
- Begin frame 开始帧
- 执行RAF(RequestAnimationFrame,不了解此api的可以自行查阅)
- 计算样式、更新布局、绘制渲染
- 执行RIC(RequestIdleCallback)
最后一步执行RIC并不是每一帧都会执行,只有当前帧内还有空余时间,并且还有未执行完的任务,才会执行此步骤。执行RIC事件不会中断,所以此事件时间不要过长,否则会影响下一帧的渲染。
RequestIdleCallback
window.requestIdleCallback()
方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout
,则有可能为了在超时前执行函数而打乱执行顺序。
为了让大家更好理解此函数怎么用,解决什么问题,我们直接看示例:
这个示例通过setInterval不断更新dom节点,在更新的同时会执行一些计算任务,通过常规执行计算任务以及使用RIC执行计算任务来帮助大家理解
DOM部分:
<div id="ui">0</div>
<div>执行<span id="test">0</span></div>
<button onclick="startNormalTask()">常规执行计算任务</button>
<button onclick="startRequestIdleCallbackTask()">
在requestIdleCallback中执行计算任务
</button>
通过setInterval一直执行dom的更新操作
setInterval(() => {
document.getElementById("ui").innerHTML =
parseInt(document.getElementById("ui").innerHTML) + 1;
}, 100);
定义如下两个任务队列:
let task1 = [
function () {
for (var i = 0; i < 3000; i++) {console.log("")}
console.log("第一个任务");
document.getElementById("test").innerHTML = 1;
},
function () {
for (var i = 0; i < 3000; i++) {console.log("")}
console.log("第一个任务");
document.getElementById("test").innerHTML = 1;
},
function () {
for (var i = 0; i < 3000; i++) {console.log("")}
console.log("第一个任务");
document.getElementById("test").innerHTML = 1;
}
];
// let task2 ...(同task1)
startNormalTask函数
function startNormalTask() {
if (task1.length > 0) {
work1(task1);
}
}
function work1(tasks) {
tasks.shift()();
console.log("执行任务");
if (task1.length > 0) {
work1(task1);
}
}
执行此函数会很明显的看到一直递增的数字会卡顿一下,也就是出现了掉帧现象
startRequestIdleCallbackTask函数
//将js计算任务放在requestIdleCallback中运算
function startRequestIdleCallbackTask() {
requestIdleCallback(myNonEssentialWork);
}
function myNonEssentialWork(deadline) {
// 如果帧内有富余的时间 并且还有未完成的任务
while (
deadline.timeRemaining() > 0 && task2.length > 0
) {
work2(task2);
}
//开启下一个空闲时间的回调函数
if (task2.length > 0) {
requestIdleCallback(myNonEssentialWork);
}
}
function work2(tasks) {
tasks.shift()();
console.log("执行任务");
}
这种时候,页面将不会出现一闪掉帧的现象,大家可以自行尝试
那么react团队为什么又polyfill这个方法呢:
- 浏览器兼容不好
- RIC的FPS只有20,也就是50ms刷新一次,影响用户的体验感
(至于React源码中具体如何实现,我研究清楚了再说)
fiber结构的类型定义以及如何关联起整个Fiber树
类型定义
src/react/node_modules/react-reconciler/src/ReactInternalTypes.js
这里只介绍几个关键的类型:
type Fiber = {
// 指向父节点
return: Fiber | null
// 指向第一个!!!孩子节点
child: Fiber | null
// 指向第一个孩子节点的兄弟节点
sibling: Fiber | null
// 指向上次生成的Fiber树 link to old Fiber
alternate: Fiber | null
}
这里较难理解的是,正常情况我们会将父节点下所有的孩子节点都作为child,可是Fiber这里是仅仅将第一个孩子节点作为child,其余的孩子节点通过第一个子节点的sibling关联,这样子构成了整颗Fiber树。
Fiber树渲染的顺序
通过一个简易版示例来理解下:
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.parent;
}
-
首先可以理解为深度优先遍历
- Root => 第一个子节点div => div的子节点h1 => h1的子节点p
-
p标签没有子节点,寻找其兄弟节点a
-
a标签既没有子节点,也没有兄弟节点,找到其父节点,渲染父节点的兄弟节点h2
-
以此类推。。。
总结
此文章并没有涉及到过多源码,主要为了能对Fiber有一个浅显的认知,了解其是什么,为什么要采用Fiber的架构,其中心思想简单来说也不过是借鉴了RIC,了解下fiber的类型定义,渲染时按照什么顺序来进行渲染,更详细的知识会在此系列继续讲解。
转载自:https://juejin.cn/post/7136215878429278216