fiber架构是基于 react的 启发式更新算法,即data->view,不是靠 显示指派,谁先调用谁就先得到执行,先进入render。启发式意思是 不通过显式的指派,而是通过优先级调度更新;

  1. 输入框输入内容触发的`更新`优先级 > 请求数据返回后触发`更新`优先级;
  2. 1、输入框输入文字,输入框应及时显示文字
  3. 2、异步请求数据后,等一会儿显示内容,用户是可以接受的;

核心思想理念

分片渲染React Fiber 将一次大的渲染任务拆分为一系列小的渲染任务,每一个任务可以在浏览器的一帧(16ms)中执行完毕,并将任务分为高优先级(用户事件等)和 低优先级任务(普通渲染任务)分别处理。

由于Fiber调度算法分成两个阶段, 第一个阶段创建DOM,实例,执行willXXX轻量hook,并且标记它的各种可能任务(sideEffect). 第二个阶段才执行它们。这时它会优先进行DOM插入或移动操作,然后才是属性样式操作,didXXX重型hook,ref。

优化主要是这两方面:
1、同步模式下的优化:先操作DOM,再设置属性

DOM插入移除变成批处理了,样式属性也变成批处理的

2、异步模式的时间分片:Reconciliation(递归 diff) 可以分为多个小任务,可以停止、恢复
Fiber 的主要目标是实现虚拟 DOM 的增量渲染,能够将渲染工作拆分成块并将其分散到多个帧的能力。

每次只做一小段,做完一段就把时间控制权交还给主线程,而不像之前长时间占用,
从而实现对任务的暂停、恢复、复用灵活控制,
这样主线程上的用户交互及动画可以快速响应,从而解决卡顿的问题。

EventLoop在繁忙状态下会让页面卡顿低效。于是需要一个时间调度器。 浏览器刚好实现一个window.requestIdleCallback() react基于这个实现了自己的调度: 插入自己的fiber,让浏览器空闲的时候主动光临这个fiber,空闲时执行忙的时候等待下次调度; 调度还可以主动打断浏览器的瞎忙,维护了自己的栈

3、优先级

  • 优先执行高优先级的任务:如交互相关的一般视为高优先级项

Fiber 其实可以算是一种编程思想,在其它语言中也有许多应用(Ruby Fiber)。
核心思想是 任务拆分和协同,主动把执行权交给主线程,使主线程有时间空挡处理其他高优先级任务。

大致原理

不同交互(用户点击交互/请求数据/用户拖拽…)触发的状态更新(比如调用this.setState)会拥有不同优先级,在源码内对应一个时间戳变量expirationTime

React会根据expirationTime的大小调度这些更新,最终实现的效果为:「用户交互」触发的更新会拥有更高的优先级,先于「请求数据」触发的更新。

高优先级意味着该更新对DOM产生的影响会更快呈现在用户面前。

React Core Team发现基于expirationTime的调度算法虽然能满足fiber树的整体优先级调度,但是不够灵活(比如无法满足局部fiber树的优先级调度(例如Suspense))

于是优先级的value,从时间戳,又被优化为 以一个32位二进制的位代表优先级的lane模型

一些有用的话

考虑到 一下就扎入fiber的实现原理的细节里,过于劝退
所以我们努力从一些研究细节的文章里,找到关键词,脑海中有个知识地图的概念之后
(或者知道要思路,哪些难点,哪些亮点)之后再有的放矢地、充满好奇心地去看这些细节~

一些关键词:
React16之前的调度器为栈调度器:链表的引入

  • 栈没有什么不好,栈显浅易懂,代码量少,但它的坏处不能随意break掉,continue掉。
  • 由于break后我们还要重新执行,所以我们需要一种链表的结构
  • 因此Reat16设法将组件的递归更新,改成链表的依次执行


Fiber 之前的调度策略 Stack Reconciler,这个策略像函数调用栈一样,递归遍历所有的 Virtual DOM 节点,进行 Diff,一旦开始无法中断,要等整棵 Virtual DOM 树计算完成之后,才将任务出栈释放主线程
而浏览器中的渲染引擎是单线程的,除了网络操作,几乎所有的操作都在这个单线程中执行,此时如果主线程上用户交互、动画等周期性任务无法立即得到处理,影响体验。

react现有架构:fiber的改造是动的内部组件层

  • 虚拟DOM层:只负责描述结构与逻辑——虚拟DOM是由JSX转译过来的,JSX的入口函数是React.createElement, 可操作空间不大
  • 内部组件层:负责组件的更新, ReactDOM.render、 setState、 forceUpdate都是与它们打交道,能让你多次setState,只执行一次真实的渲染, 在适合的时机执行你的组件实例的生命周期钩子——改造的层
  • 底层渲染层:不同的显示介质有不同的渲染方法。浏览器端,它使用元素节点,文本节点;在Native端,会调用oc, java的GUI; 在canvas中,有专门的API方法——第三大的底层API也非常稳定

react 在生成的 Virtual Dom 基础上增加了一层数据结构Fiber Node ——为了将递归遍历转变成循环遍历,
配合 requestIdleCallback API, 实现任务拆分、中断与恢复。


React通过Fiber将树的遍历变成了链表的遍历

  • 遍历的手段是:DFS(为啥选择DFS? 广度优先啥的为啥不行,遍历还有哪些手段?

React16将内部组件层改成Fiber这种数据结构
Fiber节点拥有return(父节点), child(第一个孩子), sibling(节点的右兄弟)三个属性
有了它们就足够将一棵树变成一个链表, 实现深度优化遍历。
image.png
react更新机制:

  • 在React15中,每次更新时,都是从根组件或setState后的组件开始,更新整个子树,我们唯一能做的是,在某个节点中使用SCU断开某一部分的更新,或者是优化SCU的比较效率。(shouldupdate?
  • React16则是需要将虚拟DOM转换为Fiber节点, 在规定的时间段内能转换多少fibernode,就更新多少个(对应的分片渲染设计理念)

所以,新架构下我们的更新逻辑分成了两个阶段:
1、将虚拟DOM转换成Fiber, Fiber转换成组件实例或真实DOM(不插入DOM树,插入DOM树会reflow)

React 中调用 render() 和 setState() 方法进行渲染和更新时,主要包含两个阶段:
调度阶段(Reconciler): Fiber 之前的 reconciler(被称为 Stack reconciler)是自顶向下的递归算法,遍历新数据生成新的Virtual DOM,通过 Diff 算法,找出需要更新的元素,放到更新队列中去。
(reconciler阶段对应早期版本的diff过程)
渲染阶段(Renderer): 根据所在的渲染环境,遍历更新队列,调用渲染宿主环境的 API, 将对应元素更新渲染。在浏览器中,就是更新对应的DOM元素,除浏览器外,渲染环境还可以是 Native、WebGL 等等。

requestIdleCallback

  • requestAnimationFrame 帧数控制调用
  • requestIdleCallback 闲时调用
  • web worker 多线程调用
  • IntersectionObserver 进入可视区调用

对生命周期的影响
Fiber 之前的生命周期如下:
image.png
Fiber 架构下,生命周期做了较大调整,不再推荐使用下面三个生命周期,代码中也给出了替代建议:
componentWillMount -> componentDidMount
componentWillReceiveProps -> static getDerivedStateFromProps
componentWillUpdate -> componentDidUpdate

  • 使用getDerivedStateFromProps 替换 componentWillMount 与 componentWillReceiveProps;
  • 使用getSnapshotBeforeUpdate替换componentWillUpdate;
  • 避免使用componentWillReceiveProps;

NEW
image.png
image.png

为什么要做这么大的改动呢
在 reconciler 阶段, 在更新过程中,任务按照节点为单位拆分成了一个个小工作单元
在 render 前可能会中断或恢复导致在 render 前的这些生命周期在进行一次更新时存在多次执行的情况,可能得到与预期不一致的结果。
Fiber 给出的解决方案是增加了两个新的生命周期:

getDerivedStateFromProps: 是一个静态方法,主要取代 ComponentWillXXX 生命周期,解除此类生命周期带来的副作用。
getSnapshotBeforeUpdate: 会在 render 之后执行,而执行之时 DOM 元素还没有被更新,给了一个机会去获取 DOM 信息,计算得到一个 snapshot。

调度拆分为小任务

image.png

任务优先级

确定优先级

  • 来源相同的更新,那么应该先入先出,即应该知道其时序信息
  • 不同来源的更新,笼统的来说,交互相关的优先级较高

那么,任务的优先级可以认为是大致与时间相关的一个值,但并不等于任务的产生时间,而需要经过一些处理和细节的考量

优先级运用
假设每次更新(如 setSate)都能算得一个优先级,那么该如何应用呢?
1、简单地,更新只涉及到一个 element:可以用一个类似优先队列的结构来做
2、更新涉及到多个 element:例如在树上不同元素的更新,其有不同的优先级,如何协作更新?
把每次更新提到根部,从根部开始进行所有 element 的更新
那么在遍历过程中,从上到下,对于每个 element :只处理优先级大于当前优先级的更新即可

副作用考虑:某个更新,触发了联动更新?
(一次更新中,触发了多个不同优先级的更新,那么如何决定下一次更新的优先级?)
下一次的优先级应该最好是所有优先级的最大值,所以在当期轮的更新时应做的是收集这个触发更新的信息,
以得到下轮更新的优先级

运用的具体api层面
requestIdleCallback:低优先级
低优先级的任务交给requestIdleCallback处理,这是个浏览器提供的事件循环空闲期的回调函数,需要 pollyfill,而且拥有 deadline 参数,限制执行事件,以继续切分任务;

requestAnimationFrame:高优先级
高优先级的任务交给requestAnimationFrame处理;

渲染更新:reconciliation & Commit

  • reconciliation 阶段(对应早期版本的diff过程: vdom 的数据对比,是个适合拆分的阶段,比如对比一部分树后,先暂停执行个动画调用,待完成后再回来继续比对。
  • Commit 阶段(对应早期patch阶段): 将 change list 更新到 dom 上,并不适合拆分,才能保持数据与 UI 的同步。否则可能由于阻塞 UI 更新,而导致数据更新和 UI 不一致的情况。

reconciliation:

  • 更新 state 与 props;
  • 调用生命周期钩子;
  • 生成 virtual dom:这里应该称为 Fiber Tree 更为符合;
  • 通过新旧 vdom 进行 diff 算法,获取 vdom change
  • 确定是否需要重新渲染

commit:

  • 如需要,则操作 dom 节点更新;

reconciliation:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

commit:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

生命周期

  1. class Component extends React.Component {
  2. // 替换 `componentWillReceiveProps` ,
  3. // 初始化和 update 时被调用
  4. // 静态函数,无法使用 this
  5. static getDerivedStateFromProps(nextProps, prevState) {}
  6. // 判断是否需要更新组件
  7. // 可以用于组件性能优化
  8. // 默认每次调用setState,一定会最终走到 diff 阶段,
  9. // 但可以通过shouldComponentUpdate的生命钩子返回false来直接阻止后面的逻辑执行,
  10. // 通常是用于做条件渲染,优化渲染的性能。
  11. shouldComponentUpdate(nextProps, nextState) {}
  12. // 组件被挂载后触发
  13. componentDidMount() {}
  14. // 替换 componentWillUpdate
  15. // 可以在更新之前获取最新 dom 数据
  16. # 可以在更新之前获取最新的渲染数据,它的调用是在 render 之后, update 之前;
  17. getSnapshotBeforeUpdate(prevProps, prevState) {}
  18. // 组件更新后调用
  19. # 在componentDidUpdate使用setState时,必须加条件,否则将进入死循环
  20. componentDidUpdate() {}
  21. // 组件即将销毁
  22. componentWillUnmount() {}
  23. // 组件已销毁
  24. componentDidUnmount() {}
  25. }

最佳实践

  • 在constructor初始化 state;
  • 在componentDidMount中进行事件监听,并在componentWillUnmount中解绑事件;
  • 在componentDidMount中进行数据的请求,而不是在componentWillMount;
  • 需要根据 props 更新 state 时,使用getDerivedStateFromProps(nextProps, prevState)
    旧 props 需要自己存储,以便比较; ```jsx public static getDerivedStateFromProps(nextProps, prevState) { // 当新 props 中的 data 发生变化时,同步更新到 state 上 if (nextProps.data !== prevState.data) {
    1. return {
    2. data: nextProps.data
    3. }
    } else {
    1. return null1
    } }
  1. - componentDidUpdate监听 props 或者 state 的变化,例如:
  2. ```javascript
  3. componentDidUpdate(prevProps) {
  4. // 当 id 发生变化时,重新获取数据
  5. if (this.props.id !== prevProps.id) {
  6. this.fetchData(this.props.id);
  7. }
  8. }
  9. # 在componentDidUpdate使用setState时,必须加条件,否则将进入死循环;

setState

在了解setState之前,我们先来简单了解下 React 一个包装结构: Transaction(事务)

通过事务,可以统一管理一个方法的开始与结束;处于事务流中,表示进程正在执行一些操作;
事务 (Transaction)是 React 中的一个调用结构,用于包装一个方法,
结构为: initialize - perform(method) - close。
image.png
异步与同步: setState并不是单纯的异步或同步,这其实与调用时的环境相关:
1、在 合成事件生命周期钩子(除 componentDidUpdate) 中,setState是”异步”的;
因为在setState的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents队列中等待执行;否则,开始执行batchedUpdates队列更新;

  • 在生命周期钩子调用中,组件仍处于事务流中,为异步;
    • 而componentDidUpdate是在更新之后,此时组件已经不在事务流中了,因此则会同步执行;
  • 在合成事件中,React 是基于 事务流完成的事件委托机制 实现,也是处于事务流中;

问题: 无法在setState后马上从this.state上获取更新后的值。
解决: 如果需要马上同步去获取新值,setState其实是可以传入第二个参数的。setState(updater, callback),在回调中即可获取最新值;

2、在 原生事件setTimeout 中,setState是同步的,可以马上获取更新后的值;
原生事件是浏览器本身的实现,与事务流无关,自然是同步;
而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;

批量更新
在 合成事件 和 生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。
因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新;

  • setState 合并,在 合成事件 和 生命周期钩子 中多次连续调用会被优化为一次;

函数式

由于 Fiber 及 合并 的问题,官方推荐可以传入 函数 的形式。setState(fn),在fn中返回新的state对象即可,例如this.setState((state, props) => newState);
使用函数式,可以用于避免setState的批量更新的逻辑,传入的函数将会被 顺序调用

Fiber Node 及 Fiber Tree

Fiber Node是react 在 Virtual Dom 基础上增加的一层数据结构,主要是为了将递归遍历转变成循环遍历
每一个 Fiber Node 节点与 Virtual Dom 一一对应,所有 Fiber Node 连接起来形成 Fiber tree, 是个单链表树结构,如下图所示:
image.png

扩:requestIdleCallback兼容性

React 的时间分片渲染就想要用到这个 API,不过目前浏览器支持的不给力,他们是自己去用 postMessage 实现了一套

扩:理念带来性能提升的背后原理

EventLoop是非常复杂的,但有一个要点,你一下子分配它许多任务,它的处理速度就下降。如果你把相同的任务放在一起,它就速度就上去(如className代替多个dom.style.xxx=yyy, fragment代替多个节点插入)。
一下子创建10000个DIV,并设置随机innerHTML,随机背景,它在chrome都会卡好久。
如果打散,每隔60ms处理当中的100个,分10分次处理,则页面很流畅。
(怎么分段处理带来的性能优化,被说的这么诡异了
(不就是我们熟以为常见得,同步代码转异步的操作么)

  1. //一下了吃进10000个DIV by 司徒正美
  2. function randomHexColor() { //随机生成十六进制颜色
  3. return "#" + ("00000" + (Math.random() * 0x1000000 << 0).toString(16)).substr(-6);
  4. }
  5. setTimeout(function () {
  6. var k = 0;
  7. var root = document.getElementById("root");
  8. for (var i = 0; i < 10000; i++) {
  9. k += new Date - 0;
  10. var el = document.createElement("div");
  11. el.innerHTML = k+ Math.random()
  12. root.appendChild(el);
  13. el.style.cssText = "background:" + randomHexColor() + ";height:30px;";
  14. }
  15. }, 3000);

浏览器的运作流程:渲染 -> tasks -> 渲染 -> tasks -> 渲染 -> tasks -> ….
tasks的时长不宜过长,这样浏览器就有时间优化JS代码与修正reflow!下图是我们理想中的渲染过程
image.png

React16的优化思想就是基于上面的点
由于Fiber调度算法分成两个阶段,
第一个阶段创建DOM,实例,执行willXXX轻量hook,并且标记它的各种可能任务(sideEffect).
第二个阶段才执行它们。这时它会优先进行DOM插入或移动操作,然后才是属性样式操作,didXXX重型hook,ref。
同步模式下的超级优化:
先操作DOM,再设置属性就是一个非常大的优化。DOM插入移除变成批处理了,样式属性也变成批处理的

image.png

更绝的是异步模式的时间分片:
EventLoop在繁忙状态下会让页面卡顿低效。于是需要一个时间调度器。浏览器刚好实现一个
window.requestIdleCallback()
requestIdleCallback根据参数的不同,可以在限度时间内安排一定量的JS任务,从而不影响视图渲染/事件回调;
也可以强制在浏览器不断更新视图的瞎忙中,强制中断这个行为,立即安插进我们React JS逻辑。

我们在requestIdleCallback的回调中加入一个WorkLoop的方法:
它每接触一个fiber时,就判定一下当前时间,看否有空闲时间让它进行beginWork操作
(相当于刚才的第一个阶段,设置dom, instance, willXXX),
没有就把它放进列队中。把控制权让渡给视图渲染
下次requestIdleCallback唤起时,从列队将刚才那个fiber取出来,执行beginWork。

扩:性能优化

当遇到进程阻塞的问题时,任务分割异步调用缓存策略 是三个显著的解决思路。

扩:框架设计的启发

由dom diff而来:
更新时候 的增量渲染能力,需要知道当前较之前的更新 在全局的数据中看,是更新了什么
way1:在更新调用的时候,维护这个更新差量,更新差量的合集就是最终的diff
way2:更新调用的时候,直接设置挂载全局的dom上,通过新旧dom diff的方式,知道新增diff

参考资料

React Fiber架构
React Fiber 架构学习