什么是Fiber
Fiber翻译过来是纤维,可以理解为是一个执行单元,也可以理解为是一种数据结构,个人更倾向于执行单元。
为什么要用Fiber
1.优化react的更新渲染效果
先看下React和vue的响应式更新原理
Vue的更新:由于使用静态模板,所以可以准确的找到需要更新的组件,需要进行diff的树比较小。
React更新:由于是jxs,写起来很灵活方便,但是每次更新,父子组件都会reRender,产生更大的虚拟DOM,diff的树就比较大,diff压力就会比较大。很可能执行时间较长,16ms完成不了(详细可看16ms渲染帧),导致画面卡顿。
因此react引入fiber,优化渲染。化整为零,一次diff的时间较长,那就把这个过程碎片化,分别在多个16ms帧里运行,那么问题不久解决了嘛。
-
首先碎片化,需要保存diff状态,虚拟Dom已经diff了多少先保存下来,下回再接着diff。我们知道diff过程中,对虚拟dom的遍历方式是后续遍历,而且由于是树结构,子节点中是没有父节点索引的,所以Dom数据结构上要换成双向链表,这也是Fiber可以被理解为一种数据结构的原因。
-
调度策略,首先react把dom的更新视为一个优先级较低的任务,在每个16ms帧内,会先执行用户输入,事件回调等优先级高的js任务。然后通过
requestIdleCallback(原理类似,实际react用的不是这个),在16ms的空余时间里去做diff工作。 -
render阶段和commit阶段,整个的diff过程分为render阶段和commit阶段,render就是对比过程,找出需要对真实dom进行的操作用
effect list(副作用)收集起来。commit阶段就是执行副作用,修改真实dom。render阶段是可以分段的(中断,然后继续完成),commit阶段不可中断,需要一次性完成。这也比较合理,不能每一帧都改一部分dom吧,那样式得多奇怪啊。
2.使得React18里实现Concurrent Mode(并发更新模式)成为可能。
并发模式说白了就是把react更新分为紧急更新和非紧急更新,像耗时比较长的这种更新发生的时候(非紧急更新),我们做了时间切片(即把更新分成n部分在不同帧内执行),此时会优先响应用户交互行为(input、click回调这种),但是如果这些回调里也调用了setState,需要更新页面UI怎么办。如果你不更新,你还是属于没有优先响应用户交互行为,还是被非紧急更新阻塞住了。那么用户交互产生的更新作为紧急更新,应该被优先执行,优先render。非紧急更新需要等紧急更新完,再从头重新render(必须得从头更新啊,因为你diff好的那部分东西可能数据变了,需要用最新的数据重新diff了)
关于react18的新功能,可以看看这篇文章:juejin.cn/post/709403…
requestIdleCallback
requestIdleCallback是 react Fiber 实现的基础 api 。我们希望能够快速响应用户,让用户觉得够快,不能阻塞用户的交互,requestIdleCallback能使开发者在主事件循环上执行后台和低优先级的工作,而不影响延迟关键事件,如动画和输入响应。正常帧任务完成后没超过16ms,说明有多余的空闲时间,此时就会执行requestIdleCallback里注册的任务。
-
首先需要处理输入事件,能够让用户得到最早的反馈
-
接下来是处理定时器,需要检查定时器是否到时间,并执行对应的回调
-
接下来处理 Begin Frame(开始帧),即每一帧的事件,包括 window.resize、scroll、media query change 等
-
接下来执行请求动画帧 requestAnimationFrame(rAF),即在每次绘制之前,会执行 rAF 回调
-
紧接着进行 Layout 操作,包括计算布局和更新布局,即这个元素的样式是怎样的,它应该在页面如何展示
-
接着进行 Paint 操作,得到树中每个节点的尺寸与位置等信息,浏览器针对每个元素进行内容填充
-
到这时以上的六个阶段都已经完成了,接下来处于空闲阶段(Idle Peroid),可以在这时执行 requestIdleCallback 里注册的任务
直接上代码
let taskQueue = [
() => {
console.log('task1 start')
console.log('task1 end')
},
() => {
console.log('task2 start')
console.log('task2 end')
},
() => {
console.log('task3 start')
console.log('task3 end')
}
]
const performUnitWork = () => {
// 取出第一个队列中的第一个任务并执行
taskQueue.shift()()
}
const workloop = (deadline) => {
console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`)
// 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
// 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && taskQueue.length > 0) {
performUnitWork()
}
// 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
if (taskQueue.length > 0) {
window.requestIdleCallback(workloop, { timeout: 1000 })
}
}
// 两个参数,一个是每帧的空余时间经行的回调函数,一个是超时时间,超时时会强制执行。
requestIdleCallback(workloop, { timeout: 1000 })
浏览器一帧的时间并不严格是16ms,是可以动态控制的。如果子任务的时间超过了一帧的剩余时间,则会一直卡在这里执行,直到子任务执行完毕。如果代码存在死循环,则浏览器会卡死。如果此帧的剩余时间大于0(有空闲时间)或者已经超时(上文定义了 timeout 时间为1000,必须强制执行了),且当时存在任务,则直接执行该任务。如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器。如果多个任务执行总时间小于空闲时间的话,是可以在一帧内执行多个任务的。
由于requestIdleCallback的兼容性不太好,react最后通过requestAnimationFrame + MessageChannel实现了类似效果,总体思想还是切片机制,MessageChannel是宏任务的一种,react会在每帧的requestAnimationFrame中执行一次或多次MessageChannel,来把控好每帧的执行时间。