本文已参与「新人创作礼」活动,一起开启掘金创作之路。详情
我们都知道在reactv15与reactv16版本很大一个区别在于16版本使用了fiber。那么fiber到底是什么呢?
React Fiber 是对React核心算法的重新实
那为何又要使用Fiber对React核心算法重新实现呢,原来的算法有什么问题呢?
React15/16架构
1、React15内部的运作可以分为3层
- VDOM(Virtual DOM虚拟dom),负责来描述页面
- Reconciler(协调器),主要负责找出组件的变化,负责调用组件生命周期,diff运算等
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
从v15到v16,React团队花了两年时间将源码架构中的Stack Reconciler重构为Fiber Reconciler
2、React16的内部运作模块:
- VDOM(Virtual DOM虚拟dom),负责来描述页面
- Scheduler(调度器)——调度任务的优先级,高优任务优先进入Reconciler;
- Reconciler(协调器)——负责找出变化的组件:更新工作从递归变成了可以中断的循环过程。Reconciler内部采用了Fiber的架构;
- Renderer(渲染器)——负责将变化的组件渲染到页面上。
3、为什么要将原本的Reconciler改成Fiber Reconciler呢?
- 在React15及以前,Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,递归更新时间超过了16ms,用户交互就会卡顿。
- 为了解决这个问题,React16将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟DOM数据结构已经无法满足需要。于是,全新的Fiber架构应运而生。
4、浏览器一帧都会干些什么以及requestIdleCallback的启示
上面讲到超过16ms就会卡顿,这是为什么呢?这就要说到浏览器渲染了。目前浏览器大多是60HZ(60帧/秒),所以是约16.6ms渲染一帧的画面。那么再一帧的时间内浏览器都做了什么?
- 接受输入事件
- 执行事件回调
- 开始一帧
- 执行 RAF (RequestAnimationFrame)
- 页面布局,样式计算
- 绘制渲染
- 执行 RIC (RequestIdelCallback)
第七步的 RIC 事件不是每一帧结束都会执行,只有在一帧的 16.6ms 中做完了前面 6 件事儿且还有剩余时间,才会执行。如果一帧执行结束后还有时间执行 RIC 事件,那么下一帧需要在事件执行结束才能继续渲染,所以 RIC 执行不要超过 30ms,如果长时间不将控制权交还给浏览器,会影响下一帧的渲染,导致页面出现卡顿和事件响应不及时。
requestIdleCallback 的启示:我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。
在React中实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。
5、React15的限制即16新版本的解决办法
react15的Reconciler协调过程是递归遍历组件数据生成新的虚拟DOM,然后通过diff算法,找到需要变更的元素(Patch),放到更新队列里面去。
递归过程是不可打断的,这就使得当组件树过大时,这一阶段的耗时过长,当超过一帧的时间时,主线程被占用,无法将主线程交出去渲染下一帧的内容,导致页面的卡顿。
React16 Fiber的出现就是将这个更新渲染过程给拆分成小片。每次只执行一小部分的任务,做完后若还有剩余时间(requestIdleCallback),就进行下一个任务。如果没有剩余时间了,那就把当前任务挂起,把控制权交出去进行渲染等之类的高优先级任务。
React Fiber
Fiber是什么?
React Fiber是对核心算法的一次重新实现。React Fiber把更新过程碎片化,把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会
- 在
React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断。这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来 - 因为一个更新过程可能被打断,所以
React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase
- 调度阶段(reconciliation):这个阶段是可以被打断的。在这个阶段 React 会更新数据生成新的
Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去,得到新的更新队列。 - 渲染阶段(commit):这个阶段不可以被打断。这个阶段 React 会遍历更新队列,将其所有的变更一次性更新到DOM上
React Fiber改变了之前react的组件渲染机制,新的架构使原来同步渲染的组件现在可以异步化,可中途中断渲染,执行更高优先级的任务。释放浏览器主线程。
上面这两个阶段大部分工作都是React Fiber做,和我们相关的也就是生命周期函数
Fiber核心思想
最主要的思想就是将任务拆分。
- DOM需要渲染时暂停,空闲时恢复。
window.requestIdleCallback- React内部实现的机制
Fiber关键特性
- 增量渲染(把渲染任务拆分成块,匀到多帧)
- 更新时能够暂停,终止,复用渲染任务
- 给不同类型的更新赋予优先级
- 并发方面新的基础能力
⚠组件渲染顺序
假如有A,B,C,D组件,层级结构为:
1、对于React15来说
挂载阶段执行顺序:
constructor()componentWillMount()render()componentDidMount()
挂载阶段,A,B,C,D的生命周期渲染顺序是:
以render()函数为分界线。从顶层组件开始,一直往下,直至最底层子组件。然后再往上
更新阶段为执行顺序是:
componentWillReceiveProps()shouldComponentUpdate()componentWillUpdate()render()componentDidUpdate
更新阶段同理,也是以render()函数为分界线,从顶层组件开始,依次往下顺序:A-WillReceive、A-shouldUpdate、A-WillUpdate、A-render再到B-WillReceive、B-shouldUpdate、B-WillUpdate、B-render继而再到C组件、D组件的方式渲染。
这就存在一个问题:如果这是一个很大,层级很深的组件,react渲染它需要几十甚至几百毫秒,在这期间,react会一直占用浏览器主线程,任何其他的操作(包括用户的点击,鼠标移动等操作)都无法执行
2、🎄React16之后的Fiber下的组件渲染顺序
加入fiber的react将组件更新分为两个时期
这两个时期以render为分界
-
render前的生命周期为phase1阶段1。这个阶段的生命周期是可以被打断的,每隔一段时间它会跳出当前渲染进程,去确定是否有其他更重要的任务。此过程,
React在workingProgressTree(并不是真实的virtualDomTree)上复用current上的Fiber数据结构来一步地(通过requestIdleCallback)来构建新的 tree,标记处需要更新的节点,放入队列中 -
render后的生命周期为phase2阶段2。phase2的生命周期是不可被打断的,React将其所有的变更一次性更新到DOM上
最重要的是phase1阶段所做的事。因此我们需要具体了解phase1的机制
- 如果不被打断,那么
phase1执行完会直接进入render函数,构建真实的virtualDomTree - 如果组件再
phase1过程中被打断,即当前组件只渲染到一半(也许是在willMount,也许是willUpdate~反正是在render之前的生命周期),那么react会怎么干呢?react会放弃当前组件所有干到一半的事情,去做更高优先级更重要的任务(当然,也可能是用户鼠标移动,或者其他react监听之外的任务),当所有高优先级任务执行完之后,react通过callback回到之前渲染到一半的组件,从头开始渲染。(看起来放弃已经渲染完的生命周期,会有点不合理,反而会增加渲染时长,但是react确实是这么干的)
所有phase1的生命周期函数都可能被执行多次,因为可能会被打断重来
所以,facebook在
react16增加fiber结构,其实并不是为了减少组件的渲染时间,事实上也并不会减少,最重要的是现在可以使得一些更高优先级的任务,如用户的操作能够优先执行,提高用户的体验,至少用户不会感觉到卡顿
Fiber结构
上面讲了这么多Fiber可以做到的事,那么Fiber到底是个上面结构呢?
具体来说,Fiber是一个JavaScript的对象,是指组件上将要完成或者已经完成的任务,每个组件可以一个或者多个。其中包含有关组件,以及输入和输出的信息。类似于下面这样的结构:
/**
* fiber:
* type 标记节点类型(div/span/...)
* key 标记节点在当前层级下的唯一性
* props 属性
* index 标记当前层级下的位置
* child 第一个子节点
* sibling 下一个兄弟节点
* return 父节点
* stateNode 如果组件是原生标签则是dom节点,如果是类组件则是类实例
*/
export function createFiber(vnode, returnFiber) {
const newFiber = {
type: vnode.type,
key: vnode.key,
props: vnode.props,
stateNode: null,
child: null,
return: returnFiber,
sibling: null,
alternate: null,
flags: Placement,
...
};
return newFiber;
}
- type和key字段:这两个字段在reconciliation期间确定Fiber是否可重用。
- child和sibling字段:这些字段指向 第一个子节点/下一个兄弟节点 的Fiber结构。这就使得dom组件的树状结构通过这样一个Fiber指向一个Fiber的过程变成了链表结构。
- return字段:return fiber 是当程序处理完当前fiber之后返回的fiber。从概念上讲,视为返回父fiber。
- 当我们使用链表结构的Fiber去diff时,便可以做到”被打断“
React Fiber架构总结
🕖更新的两个阶段
- 调度算法阶段-执行diff算法,纯js计算
- Commit阶段-将diff结果渲染dom
⚠可能会有性能问题
- JS是单线程的,且和DOM渲染公用一个线程
- 当组件足够复杂,组件更新时计算和渲染压力都大
- 同时再有DOM操作需求(动画、鼠标拖拽等),将卡顿
✔解决方案Fiber
-
将调度算法阶段阶段任务拆分(Commit无法拆分)
-
DOM需要渲染时暂停,空闲时恢复
-
分散执行:任务分割后,就可以把小任务单元分散到浏览器的空闲期间去排队执行,而实现的关键是两个新API:
requestIdleCallback与requestAnimationFrame- 低优先级的任务交给
requestIdleCallback处理,这是个浏览器提供的事件循环空闲期的回调函数,需要pollyfill,而且拥有deadline参数,限制执行事件,以继续切分任务; - 高优先级的任务交给
requestAnimationFrame处理;
- 低优先级的任务交给
🏆Fiber结构带来的改变
Fiber 这里可以具象为一个 数据结构,讲树状结构描述为链表形式
1、链表树遍历算法:通过 节点保存与映射,便能够随时地进行 停止和重启,这样便能达到实现任务分割的基本前提
- 首先通过不断遍历子节点,到树末尾;
- 开始通过
sibling遍历兄弟节点; - return 返回父节点,继续执行2;
- 直到 root 节点后,跳出遍历;
2、任务分割
,React 中的渲染更新可以分成两个阶段
- reconciliation 阶段: vdom 的数据对比,是个适合拆分的阶段,比如对比一部分树后,先暂停执行个动画调用,待完成后再回来继续比对
- Commit 阶段: 将 change list 更新到 dom 上,并不适合拆分,才能保持数据与 UI 的同步。否则可能由于阻塞 UI 更新,而导致数据更新和 UI 不一致的情况
3、分散执行: 任务分割后,通过requestIdleCallback与requestAnimationFrame这两个API把小任务单元分散到浏览器的空闲期间去排队执行。
// 类似于这样的方式
requestIdleCallback((deadline) => {
// 当有空闲时间时,我们执行一个组件渲染;
// 把任务塞到一个个碎片时间中去;
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) && nextComponent) {
nextComponent = performWork(nextComponent);
}
});
4、优先级策略: 文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防将来会显示的任务
- Fiber 其实可以算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber)。
- 核心思想是 任务拆分和协同,主动把执行权交给主线程,使主线程有时间空挡处理其他高优先级任务。
- 当遇到进程阻塞的问题时,任务分割、异步调用 和 缓存策略 是三个显著的解决思路。