图片 1

使用React的同学们都应该要知道React Fiber,因为这玩意快要正式发布了。

原文链接-https://github.com/AlloyTeam/omi/tree/master/tutorial

        在《JavaScript异步机制》这篇文章中我们说到,Js引擎是单线程的,它负责维护任务栈,并通过
Event Loop
的机制,按顺序把任务放入栈中执行,React的底层也是javaScript,因此他也不置可否的必须按照js的规则执行。那么React的优势在哪呢?我们接着往下看。

背景

React Fiber这个大改变Facebook已经折腾两年多了,刚刚结束不久的React Conf
2017会议上,Facebook终于确认,React
Fiber会搭上React下一个大版本v16的顺风车发布。

写在前面

Omi框架在架构设计的时候就决定把update的控制权交给了开发者,视灵活性比生命还重要。不然的话,如果遇到React
Fiber要解决的这类问题的话,就需要推翻原有架构重新搞了。

        稍有经验的前端工程师会知道,页面的DOM改变,就会导致页面重新计算DOM,进行重绘或者重排,DOM结构复杂或者频繁操作DOM通常是性能瓶颈产生的原因。而网站从最开始比较简单,开始变的越来越复杂,用户交互也会越来越多,怎么去减轻DOM操作带来的性能损耗就变得重要起来。这时候,我们的React带着强大的创造性,顺应时代而生,Virtual
DOM的提出引领了前端的变革,Virtual DOM 是一个 JavaScript
对象。每次,我们只需要告诉 React
下一个状态是什么,React会自己构建一个新的 Virtual
DOM,然后根据又一史诗性的创造–React diff算法(之前也着重剖析过React
diff的源码,传送门)快速计算其差异,找出需要重绘或重排的元素,告诉浏览器。浏览器根据相关的更新,重新计算
DOM Tree,重绘页面。我们来看一个例子:

前段时间准备前端招聘事项,复习前端React相关知识;复习React16新的生命周期:弃用了componentWillMountcomponentWillReceivePorpscomponentWillUpdate三个生命周期,
新增了getDerivedStateFromPropsgetSnapshotBeforeUpdate来代替弃用的三个钩子函数。发现React生命周期的文章很少说到
React 官方为什么要弃用这三生命周期的原因,
查阅相关资料了解到根本原因是V16版本重构核心算法架构:React
Fiber
;查阅资料过程中对React
Fiber
有了一定了解,本文就相关资料整理出个人对Fiber的理解,
与大家一起简单认识下React Fiber

在 Is Fiber Ready Yet? 这个网站上可以看到当前React
Fiber的单元测试通过率,我刚刚(北京时间2017年3月28日下午1:41)看到的是93.6%,据说Facebook在自己的网站已经将React
Fiber投入实战,所以,React Fiber大势所趋,是时候了解一下React Fiber了。

React Fiber

先引用下我们团队小鲜肉Stark伟-复旦大四 /
腾讯@AlloyTeam在知乎上的回答

React 的核心思想是每次对于界面 state 的改动,都会重新渲染整个 virtual
dom,然后新老的两个 virtual dom 树进行 diff,对比出变化的地方,然后通过
renderer
渲染到实际的UI界面(这里可能是浏览器的DOM,也可能是native组件)。这样实质上就是把界面变成一个纯粹的状态机,React
的作用就是把这个状态机之间的状态转换高效率地运行出来。但是存在以下问题:

  • 1、不是每一次状态的变化都要立刻执行。
  • 2、不同的状态变化之间是有轻重缓急之分的,比如『动画』这种状态变化的优先级,出于对用户体验的考量,为了避免动画卡顿或者掉帧,一般比『改变页面数据』的优先级更高。
  • 3、我们现在的做法只是调用 setState 触发重新渲染,然后 React
    会收集一个 tick 内的 state
    变化,然后执行,所以有可能大量的计算会在同一时刻阻塞进程。但我们没法控制
    React
    运算的时序问题,也不太可能通过手工声明让动画的优先级比数据变更更高。而
    React
    作为一个用户交互的框架,它本应该能让程序员能控制这些东西。所以这个破事要怎么解决咧?(
    ⊙ o ⊙ )我们知道,任何的函数调用都会有自己的调用栈,比如对于 v = f(d)
    这个函数来说,函数 f 可能又调用了一系列其它的函数,这些函数就包括在
    f
    的调用栈中。关键的问题在于,这种原生的调用栈是基本不可延迟的,它会立即执行,如果计算量很大的话就会阻塞住进程,让界面失去响应,这种事情经常发生在
    React 的渲染过程中。

或者看颜什么都不记得适的回答:

状态转移时,是在一次 tick 中递归遍历组件树,找出需要更新的节点
rerender。但是这样造成了一些问题:

  • 在 UI
    中,不是所有的状态转移都需要立即执行。大量的同时计算可能会导致资源的浪费,以致出现掉帧的状况,降低用户体验。
  • 不同类型的状态转移应有不同的优先级,比如点击按钮出现动画的优先级应该比
    Fetch API 要高。
  • React 是 pull-based 实现的,事务的时序全部由 React
    决定。我们没办法操控执行事务的时序。

图片 2

React Fiber是什么?

React Fiber是个什么东西呢?官方的一句话解释是“React
Fiber是对核心算法的一次重新实现”。这么说似乎太虚无缥缈,所以还是要详细说一下。

Omi component update

Omi有上面的问题吗? 没有。

Omi的卖点之一便是:更自由的更新,每个组件都有update方法,自由选择你认为最佳的时机进行更新。这样设计的一大好处是更加灵活,如果想要自动更新集成个mobx或者obajs便可,进可功退可守护。
数据和视图虽然是关系密切,但是解耦的设计还是非常必要,这样可以应付更多的场景。好处:

  • 你可以等某个动画播放完成再进行update
  • 你可以控制update顺序
  • 你可以update前后干一些事情而不需要利用生命周期的钩子函数(有的时候钩子函数让连续的逻辑打得粉碎…)

component update说完了吗?没有… Omi不仅仅有component
update!还有更加强大的 updateSelf。

1

官方的一句话解释是“React Fiber是对核心算法的一次重新实现”。Fiber
架构调整很早就官宣了,但官方经过两年时间才在V16版本正式发布。官方概念解释太笼统,
其实简单来说 React Fiber 是一个新的任务调和器, 本文后续将详细解释。

首先,不用太紧张,不要以为React
Fiber的到来是一场大革命,实际上,对我们只是把React当做工具的开发者来说,很可能感觉不到有什么功能变化。等到React
v16发布的时候,我们修改package.json中的react版本号,重新npm
install,一切就搞定了,然后我们就感觉到网页性能更高了一些,如此而已。

Omi component updateSelf

先说下两者的区别:

  • update: 更新组件树
  • updateSelf: 更新组件(不包含任何子节点)

如下图所示:

图片 3

图片 4

标红的代表会进行更新的节点。

这个例子会在页面中创建一个输入框,一个按钮,一个 BlockList
组件。BlockList 组件会根据 NUMBER_OF_BLOCK
的数值渲染出对应数量的数字显示框,数字显示框显示点击按钮的次数。我们最开始设置
据 NUMBER_OF_BLOCK 为 2 ,只渲染 2 个数字显示框。

为什么叫 “Fiber”?

当然,上面说的是“理想情况”,React
Fiber对算法的修改,还是会带来一些功能逻辑的变化,后面会说。

场景模拟

class TestComponent extends Omi.Component {
    render () {
        return `<div>
                    <h3>{{title}}</h3>
                    <List  name="list" data="listData" />
                </div>`;
    }
}

组件结构上面代码所示:

  • 如果调用组件实例的update的话,会更新组件本身以及 List组件
  • 如果调用组件实例的updateSelft的话,会更新组件本身,不会更新List组件

比如我们仅仅修改了this.data.title,就可以调用this.updateSelf方法,虽然一般情况下无脑update也能达到同样的结果,虽然morphdom的DOM
diff已经足够轻量快速,但是一定没有updateSelf方法快速。上面的例子updateSelf优势可能不明显,如果这样呢:

class TestComponent extends Omi.Component {
    render () {
        return `<div>
                    <h3>{{title}}</h3>
                    <List  name="list" data="listData" />
                    <List  name="list" data="listData" />
                    <Content  name="list" data="listData" />
                    <Slider  name="list" data="listData" />
                </div>`;
    }
}

再或者Content、Slider里面再嵌套了子组件,子组件又嵌套了子组件,如果仅仅只是需要修改title的话,updateSelf优势就尽显无疑。

首次渲染出页面之后,我们点击按钮,页面中的数字显示框的值由 0 变为
1。如下图所示:

大家应该都清楚进程线程的概念,进程是操作系统分配资源的最小单元,线程是操作系统调度的最小单元,在计算机科学中还有一个概念叫做Fiber,英文含义就是“纤维”,意指比Thread更细的线,也就是比线程(Thread)控制得更精密的并发处理机制。上面说的Fiber和React
Fiber不是相同的概念,但是,React团队把这个功能命名为Fiber,含义也是更加紧密的处理机制,比Thread更细。

为什么Facebook要搞React
Fiber呢?我们先要了解现在React(也就是直到目前最新的v15版本)的局限。

实现细节

这里主要说一说updateSelf的实现细节。主要包含两点:

  • 不重新render的情况下拿到子组件的完整的HTML
  • 关闭子组件的DOM diff

进行updateSelf的时候,就算子组件的data发生了变化,也不去改变子组件。因为updateSelf就意思就是更新自身。
所以子组件的HTML不需要使用模板和data生成,只需要component.node.outerHTML就可以了。outerHTML在古老的firefox是不支持的,可以通过创建节点插入然后读innerHTML进行polyfill。

组件本身的HTML是需要使用模板和data生成,子组件就使用刚刚的outerHTML替换便可。但是问题来了,子组件的DOM
diff其实是没有必要的,虽然morphdom的DOM
diff已经足够轻量快速。但是子组件他们本来就是一模一样,没有必要的开销。所以需要关闭DOM
diff~~。然后morphdom没有ignore相关的配置….

图片 5

Fiber 架构解决了什么问题?

同步更新过程的局限

在现有React中,更新过程是同步的,这可能会导致性能问题。

当React决定要加载或者更新组件树时,会做很多事,比如调用各个组件的生命周期函数,计算和比对Virtual
DOM,最后更新DOM树,这整个过程是同步进行的,也就是说只要一个加载或者更新过程开始,那React就以不破楼兰终不还的气概,一鼓作气运行到底,中途绝不停歇。

表面上看,这样的设计也是挺合理的,因为更新过程不会有任何I/O操作嘛,完全是CPU计算,所以无需异步操作,的确只要一路狂奔就行了,但是,当组件树比较庞大的时候,问题就来了。

假如更新一个组件需要1毫秒,如果有200个组件要更新,那就需要200毫秒,在这200毫秒的更新过程中,浏览器那个唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。想象一下,在这200毫秒内,用户往一个input元素中输入点什么,敲击键盘也不会获得响应,因为渲染输入按键结果也是浏览器主线程的工作,但是浏览器主线程被React占着呢,抽不出空,最后的结果就是用户敲了按键看不到反应,等React更新过程结束之后,咔咔咔那些按键一下子出现在input元素里了。

这就是所谓的界面卡顿,很不好的用户体验。

现有的React版本,当组件树很大的时候就会出现这种问题,因为更新过程是同步地一层组件套一层组件,逐渐深入的过程,在更新完所有组件之前不停止,函数的调用栈就像下图这样,调用得很深,而且很长时间不会返回。

图片 6因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React
Fiber就是要改变现状。

扩展 morphdom

API:

 morphdom(node, newNodeHTML, {
                        ignoreAttr: ['attr1','attr2']
                    } )

比如上面代表只要标记了attr1或者attr2的就是忽略,当然为了规避错误,这里需要严格的匹配才会ignore
DOM diff。怎么算严格的匹配?就是:

  • 当同样的attr的DOM,并且该attr在ignoreAttr里才会ignore DOM diff

2

为什么官方要花2年多的时间来重构React 核心算法?首先要从Fiber算法架构前
React 存在的问题说起!说起React算法架构避不开“Reconciliaton”。

React Fiber的方式

破解JavaScript中同步操作时间过长的方法其实很简单——分片。

把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就不会被独占,其他任务依然有运行的机会。

React
Fiber把更新过程碎片化,执行过程如下面的图所示,每执行完一段更新过程,就把控制权交还给React负责任务协调的模块,看看有没有其他紧急任务要做,如果没有就继续去更新,如果有紧急任务,那就去做紧急任务。

维护每一个分片的数据结构,就是Fiber。

有了分片之后,更新过程的调用栈如下图所示,中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。

图片 7

道理很简单,但是React实现这一点却不容易,要不然怎么折腾了两年多!

对具体数据结构原理感兴趣的同学可以去看Lin Clark在React Conf
2017上的演讲 (要翻墙的),本文中的介绍图片也出自这个演讲。

Omi Store体系addSelfView

Omi
Store体系以前通过addView进行视图收集,store进行update的时候会调用组件的update。

与此同时,Omi Store体系也新增了addSelfView的API。

  • addView 收集该组件视图,store进行update的时候会调用组件的update
  • addSelfView
    收集该组件本身的视图,store进行update的时候会调用组件的updateSelf

当然,store内部会对视图进行合并,比如addView里面加进去的所有视图有父子关系的,会把子组件去掉。爷孙关系的会把孙组件去掉。addSelfView收集的组件在addView里已经收集的也去进行合并去重,等等一系列合并优化。

当点击按钮的时候,按钮点击次数从 0 变为 1,我们需要告诉 React
下面你要显示 1 了,于是,通过 setState 操作,我们告诉 React:
下一个你需要显示的数据是 1。然后,React 开始更新组件。对应的 Virtual DOM
Tree 变化如下图所示。黄色表示状态被更新。

Reconciliation

为什么叫Fiber呢?

大家应该都清楚进程(Process)和线程(Thread)的概念,在计算机科学中还有一个概念叫做Fiber,英文含义就是“纤维”,意指比Thread更细的线,也就是比线程(Thread)控制得更精密的并发处理机制。

上面说的Fiber和React
Fiber不是相同的概念,但是,我相信,React团队把这个功能命名为Fiber,含义也是更加紧密的处理机制,比Thread更细。

说个题外话,很多年前,我听一个前辈讲课,他说到Fiber这么一回事,我就问他:怎么让我的程序进入可以操控Fiber的状态?前辈的回答是:你的程序真的需要用到Fiber吗?如果现在的方法就能满足需求,根本就不需要知道Fiber是什么东西。

呵呵,前辈说的当时我还不大理解,后来越来越觉得前辈说得有道理,如果根本没有必要用上一样东西,那就不用也罢,不要因为那个东西酷就去用,不然很可能就是自找苦吃。

扯远了,我想说的是,其实对大部分React使用者来说,也不用深究React
Fiber是如何实现的,除非实现方式真的对我们的使用方式有影响,我们也不用要学会包子怎么做的才吃包子对不对?

但是,React Fiber的实现改变还真的让我们的代码方式要做一些调整。

Omi相关

  • Omi官网omijs.org
  • Omi的Github地址
  • 如果想体验一下Omi框架,可以访问 Omi
    Playground
  • 如果想使用Omi框架或者开发完善Omi框架,可以访问
    Omi使用文档
  • 如果你想获得更佳的阅读体验,可以访问 Docs
    Website
  • 如果你懒得搭建项目脚手架,可以试试
    omi-cli
  • 如果你有Omi相关的问题可以 New
    issue
  • 如果想更加方便的交流关于Omi的一切可以加入QQ的Omi交流群(256426170)

图片 8

图片 9

React 官方核心算法名称是Reconciliation, 中文翻译是“协调”!React diff
算法的实现就与之相关。先简单回顾下React Diff: React首创了“虚拟DOM”概念,
“虚拟DOM”能火并流行起来主要原因在于该概念对前端性能优化的突破性创新;稍微了解浏览器加载页面原理的前端同学都知道网页性能问题大都出现在DOM节点频繁操作上;而React通过“虚拟DOM”

React Fiber对现有代码的影响

理想情况下,React
Fiber应该完全不影响现有代码,但可惜并完全是这样,要吃这个包子还真要知道一点这个包子怎么做的,你如果不喜欢吃甜的就不要吃糖包子,对不对?

在React
Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。

因为一个更新过程可能被打断,所以React
Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation
Phase和第二阶段Commit Phase。

在第一阶段Reconciliation Phase,React
Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的;但是到了第二阶段Commit
Phase,那就一鼓作气把DOM更新完,绝不会被打断。

这两个阶段大部分工作都是React Fiber做,和我们相关的也就是生命周期函数。

以render函数为界,第一阶段可能会调用下面这些生命周期函数,说是“可能会调用”是因为不同生命周期调用的函数不同。

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

下面这些生命周期函数则会在第二阶段调用。

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

图片 10因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React
Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次;在React
Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用!

图片 11

使用React
Fiber之后,一定要检查一下第一阶段相关的这些生命周期函数,看看有没有逻辑是假设在一个更新过程中只调用一次,有的话就要改了。

我们挨个看一看这些可能被重复调用的函数。

componentWillReceiveProps,即使当前组件不更新,只要父组件更新也会引发这个函数被调用,所以多调用几次没啥,通过!

shouldComponentUpdate,这函数的作用就是返回一个true或者false,不应该有任何副作用,多调用几次也无妨,通过!

render,应该是纯函数,多调用几次无妨,通过!

只剩下componentWillMount和componentWillUpdate这两个函数往往包含副作用,所以当使用React
Fiber的时候一定要重点看这两个函数的实现。

3

  • React Diff算法保证了前端性能;

怎么试用React Fiber?

虽然React Fiber还没有正式发布,现在你就可以试用React
Fiber,用这种方式安装react和react-dom

yarn add react@next react-dom@next

我试着将现有项目的React替换成React
Fiber之后,代码没有任何改变,可以正常运行,说明我的应用里生命周期函数应该没有假设只会被调用一次。

我相信当React
Fiber正式发布的时候,会有更清晰详尽的文档,到时候我们再来详细介绍。

长按图片识别图中二维码(或搜索微信公众号FrontEndStory)关注**“前端那些事儿”,带你探索前端的奥秘。**

图片 12

我们点击按钮,触发 setState 之后,React 就会创建一个新的 Virtual
DOM,然后将新旧 Virtual DOM 进行 diff
操作,判断哪些元素需要更新,将需要更新的元素放到更新列表中,最后遍历更新列表更新所有的元素,这所有的过程都是
React 帮我们完成的。对浏览器而言,这个过程仅仅是编译执行了一段
JavaScript 代码而已,我们把从 setState
开始,到页面渲染结束的浏览器主线程工作流程画出来,如下图所示。蓝色粗线表示浏览器主线程。

传统Diff算法

图片 13

通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3)
,n是树的节点数,这个有多可怕呢?——如果要展示1000个节点,得执行上亿次比较。。即便是CPU快能执行30亿条命令,也很难在一秒内计算出差异。

4

React Diff算法

从获得最新的数据,到将数据在页面中呈现出来,可以分为两个阶段。

将Virtual DOM树转换成actual DOM树的最少操作的过程 称为 协调。React
Diff三大策略:1.tree diff;2.component diff;3.element diff;PS:
之前H5开发遇到的State 中变量更新但视图未更新的Bug就是element
diff检测导致。解决方案:1.两种业务场景下的DOM节点尽量避免雷同;
2.两种业务场景下的DOM节点样式避免雷同;

第一个 调度阶段。这个阶段 React 用新数据生成新的 Virtual DOM ,遍历
Virtual DOM ,然后通过 Diff
算法,快速找出需要更新的元素,放到更新队列中去。

在V16版本之前协调机制Stack reconciler, V16版本发布Fiber
架构后是Fiber reconciler

第二个 渲染阶段。这个阶段 React
根据所在的渲染环境,遍历更新队列,将对应元素更新。在浏览器中,就是跟新对应的DOM元素。除浏览器外,渲染环境还可以是
Native,硬件,VR 等。

Stack reconcilerStack reconciler 源码

新问题

之前,React 在官网中写道:

We built React to solve one problem: building large applications with
data that changes over time.

现在更新为:

React is a declarative, efficient, and flexible JavaScript library for
building user interfaces.

所以我们看出,React
新的定位在于灵活高效的数据。但是在实际的使用中,尤其是遇到页面结构复杂,数据更新频繁的应用的时候,React
的表现不尽如人意。在上一个例子中,我们可以设置 NUMBER_OF_BLOCK 的值为
100000(实际情况下,可能没有那么多),将其变为一个“复杂”的网页。

点击按钮,触发 setState,页面开始更新。

点击输入框,输入一些字符串,比如
“hireact”。我们可以看到,页面此时没有任何的响应。

等待 7 s,输入框中突然出现了,之前输入的 “hireact”,同时, BlockList
组件也更新了。

在这等待 7 s
中,页面不会给我任何的响应,我会以为网站崩溃了,或者电脑死机了。如果没有让我等待几秒,只是等待了0.5秒,多等待几个0.5秒之后我会在心里默想:这是什么破网站!

显而易见,这样的用户体验并不好。

将浏览器主线程在这 7 s 的 performance 如下图所示:

图片 14

5

// React V15: react/src/renderers/shared/stack/reconciler/ReactCompositeComponent.js/** * ------------------ The Life-Cycle of a Composite Component ------------------ * * - constructor: Initialization of state. The instance is now retained. * - componentWillMount * - render * - [children's constructors] // 子组件constructor() * - [children's componentWillMount and render] // 子组件willmount render * - [children's componentDidMount] // 子组件先于父组件完成挂载didmount * - componentDidMount * * Update Phases: * - componentWillReceiveProps (only called if parent updated) * - shouldComponentUpdate * - componentWillUpdate * - render * - [children's constructors or receive props phases] * - componentDidUpdate * * - componentWillUnmount * - [children's componentWillUnmount] * - [children destroyed] * - (destroyed): The instance is now blank, released by React and ready for GC. * * -----------------------------------------------------------------------------

Fiber

可以确定的是复杂度为常数的 diff 算法还是很优秀的,主要问题出现在,React
的调度策略 — Stack
Reconfile。这个策略像函数调用栈一样,会深度优先遍历所有的 Virtual DOM
节点,进行Diff。它一定要等整棵 Virtual DOM
计算完成之后,才将任务出栈释放主线程。重点在于,Stack
Reconfile始终会一次性地同步处理整个组件树。Stack
Reconciler无法暂停,因此如果更新较为深入并且可用CPU时间有限,这种做法并非最优化的。
所以,在浏览器主线程被
React更新状态任务占据的时候,用户与浏览器进行任何的交互都不能得到反馈,只有等到任务结束,才能突然得到浏览器的响应。

React 这样的调度策略对动画的支持也不好。如果 React
更新一次状态,占用浏览器主线程的时间超过 16.6
ms,就会被人眼发现前后两帧不连续,给用户呈现出动画卡顿的效果。

React
核心团队很早之前就预知这样的风险的存在,并且持续探索可解决的方式。基于浏览器对requestIdleCallback和requestAnimationFrame这两个API
的支持,以及其他团队对者两个API的实现,如 React Native 团队。React
团队实现新的调度策略 — Fiber reconcile。

Fiber是一种轻量的执行线程,同线程一样共享定址空间,线程靠系统调度,并且是抢占式多任务处理,Fiber
则是自调用,协作式多任务处理。

Fiber Reconcile 与 Stack Reconcile 主要有两方面的不同。

首先,使用协作式多任务处理任务。将原来的整个 Virtual DOM
的更新任务拆分成一个个小的任务。每次做完一个小任务之后,放弃一下自己的执行将主线程空闲出来,看看有没有其他的任务。如果有的话,就暂停本次任务,执行其他的任务,如果没有的话,就继续下一个任务。

整个页面更新并重渲染过程分为两个阶段。

    1、Reconcile 阶段。此阶段中,依序遍历组件,通过diff
算法,判断组件是否需要更新,给需要更新的组件加上tag。遍历完之后,将所有带有tag的组件加到一个数组中。这个阶段的任务可以被打断。

    2、Commit 阶段。根据在 Reconcile
阶段生成的数组,遍历更新DOM,这个阶段需要一次性执行完。如果是在其他的渲染环境
— Native,硬件,就会更新对应的元素。

所以之前浏览器主线程执行更新任务的执行流程就变成了这样:

图片 15

Fiber1

其次,对任务进行优先级划分。不是每来一个新任务,就要放弃现执行任务,转而执行新任务。与我们做事情一样,将任务划分优先级,只有当比现任务优先级高的任务来了,才需要放弃现任务的执行。比如说,屏幕外元素的渲染和更新任务的优先级应该小于响应用户输入任务。若现在进行屏幕外组件状态更新,用户又在输入,浏览器就应该先执行响应用户输入任务。浏览器主线程任务执行流程如下图所示:

图片 16

Fiber2

我们重写一个组件,跟之前的一样。一个输入框,一个按钮,一个 BlockList
组件。BlockList 组件会根据NUMBER_OF_BLOCK
的数值渲染出对应数量的数字显示框,数字显示框显示点击按钮的次数。将
NUMBER_OF_BLOCK 设置为 100000,模拟一个复杂的页面。不同的是,使用
Fiber reconcile
调度策略,设置任务优先级,让浏览器先响应用户输入再执行组件更新。

图片 17

5

在对比代码差异之前,我们先执行同样的操作,对比一下浏览器的行为。

点击 button,触发 setState,页面开始更新。

点击输入框,输入一些字符串,比如
“hireact”。我们可以看到,页面能够响应我们的输入了。

浏览器主线程的 performance 如下图所示:

图片 18

5

可以看到,在黄色 JavaScript 执行过程中,也就是 React
占用浏览器主线程期间,浏览器在也在重新计算 DOM
Tree,并且进行重绘,截图显示,浏览器渲染的就是用户新输入的内容。简单说,在
React
占用浏览器主线程期间,浏览器也在与用户交互。这个才是我们在网站上面期望获得的体验,浏览器总是对我的输入有反馈。

那我们的代码改变了哪些呢?从下往上看:

首先,从 reactDOM.render() 变成了 ReactDOMFiber.render()。我们使用了
ReactFiber 去渲染整个页面,ReactFiber
会将整个更新任务分成若干个小的更新任务,然后设置一些任务默认的优先级。每执行完一个小任务之后,会释放主线程。

其次,render 方法中返回的不再是一个被 div
元素包一层的组件列表,而是直接返回一个组件列表,这是 React
在新版中提供的新的写法。除此之外,可以直接返回字符串和数字。像下面:

render() {

    return ‘Hi, ReactFiber!’

}

render() {

        return 123

}

再次,我们传给 setState 的不是最新状态,而是一个 callback,这个 callback
返回最新状态。同上,这个也是 React
新版中提供的新的写法,同时也是推荐的写法。

最后,我们没有直接调用 setState,而是将其作为 callback 传给了
unstable_deferredUpdates 这个 API。从名字就可以看出,deferredUpdates
是将更新推迟,unstable
表明现在还不稳定,在开发阶段。从源代码上看,unstable_deferredUpdates
做了一件事情,就是将传给它的更新任务的优先级设置为
lowpriority。所以我们将seState 作为 callback 传给了
unstable_deferredUpdates,就是告诉 React,这个 setState 任务,是一个
lowpriority 的任务。(需要注意的是,并不确定 React 团队是否将
unstable_deferredUpdates 或者deferredUpdates
作为一个开放的接口,现在这个版本[2]可以通过这个API去设置优先级。同时,从源代码可以看到,React
团队想要实现给任务设置优先级的功能,目前只看到一个 performWithPriority
的接口,也还没有实现。)

我们点击按钮之后,unstable_deferredUpdates 将这个更新任务设置为 low
priority。此时是没有其他任务存在的,React
就开始进行状态更新了。更新任务进入了 Reconcile
阶段,我们点击输入框,此时,用户交互任务来了,此任务优先级高于更新任务,所以浏览器主线程将焦点放在了输入框……。之后更新任务进入了
Commit
阶段,不能将浏览器主线程放弃,到了最后浏览器渲染完成之后,将用户在更新任务
Commit 阶段的输入以及最新的状态显示出来。

对比 Stack Reconcile 和 Fiber Reconfile 的实现,我们可以看到 React
新的调度策略让开发者对 React
应用有了更细节的控制。开发者,可以通过优先级,控制不同类型任务的优先级,提高用户体验,以及整个应用程序的灵活性。

总结起来 ,Fiber Reconfile有着以下特性:

    1、能够将可中断的任务拆分成块。

    2、能够对进程中的工作划分优先级、重新设定基址(Rebase)、恢复。

    3、能够在父子之间来回反复,借此为React的Layout提供支持。

    4、能够通过render()返回多个元素。

    5、为错误边界提供了更好的支持。

简单来说,此时不在需要等待变更传播到整个组件树,React
Fiber可以知道如何时不时暂停一下,检查是否有其他更重要的更新。这种调度能力主要基于requestIdleCallback的使用,而这是一种W3C候选推荐标准。

Stack reconciler 存在的问题

写在最后

看起来 React Fiber
很厉害的样子,如果要用的话,还是有一些问题是需要考虑的。

比如说,task 按照优先级之后,可能低优先级的任务永远不会执行,称之为
starvation;

比如说,task
有可能被打断,需要重新执行,那么某些依赖生命周期实现的业务逻辑可能会受到影响。

……

React Fiber 也是带来了很多的好处的。

比如说,增强了某些领域的支持,比如动画、布局和手势;

比如说,在复杂页面,对用户的反馈会更及时,应用的用户体验会变好,简单页面看不到明显的差异;

比如说,api基本上没有变化,对现有项目很友好。

……

现在,React Fiber 已经通过了所有的测试,在网站Is Fiber Ready
Yet?,上显示,还有4个warning需要fix。它会随着
React 16 发布,到底效果怎么样,只有用过才知道了。

Stack
reconciler的工作流程很像函数的调用过程。父组件里调子组件,可以类比为函数的递归。在setState后,react会立即开始reconciliation过程,从父节点开始遍历,以找出不同。将所有的Virtual
DOM遍历完成后,reconciler才能给出当前需要修改真实DOM的信息,并传递给renderer,进行渲染,然后屏幕上才会显示此次更新内容。对于特别庞大的DOM树来说,reconciliation过程会很长(x00ms),在这期间,主线程是被js占用的,因此任何交互、布局、渲染都会停止,给用户的感觉就是页面被卡住了。

参考资料:

Lin Clark – A Cartoon Intro to Fiber – React Conf
2017

React Fiber
Architecture

How React Fiber can make your web and mobile apps smoother and more
responsive

How Browsers Work: Behind the scenes of modern web
browsers

Tutorial: Intro To
React

*本文中 React 的版本是 React@16.0.0-alpha.3

本文所有的例子和图均来自刘杰凤老师的文章《React的新引擎—React
Fiber是什么?》,感谢~

至此,React源码剖析剖析的部分就写完了,下面准备写一写React技术栈的另一个伟大的部分——Redux,请大家期待(~ ̄▽ ̄)~

网友测试使用React V15,当DOM节点数量达到100000时,
加载页面时间竟然要7秒;详情当然以上极端情况一般不会出现,官方为了解决这种特殊情况。在Fiber
架构中使用了Fiber reconciler。

Fiber reconciler

原来的React更新任务是采用递归形式,那么现在如果任务想中断,
在递归中是很难处理,
所以React改成了大循环模式,修改了生命周期也是因为任务可中断

Fiber reconciler 源码

React的相关代码都放在packages文件夹里。

├── packages --------------------- React实现的相关代码│ ├── create-subscription ------ 在组件里订阅额外数据的工具│ ├── events ------------------- React事件相关│ ├── react -------------------- 组件与虚拟DOM模型│ ├── react-art ---------------- 画图相关库│ ├── react-dom ---------------- ReactDom│ ├── react-native-renderer ---- ReactNative│ ├── react-reconciler --------- React调制器│ ├── react-scheduler ---------- 规划React初始化,更新等等│ ├── react-test-renderer ------ 实验性的React渲染器│ ├── shared ------------------- 公共代码│ ├── simple-cache-provider ---- 为React应用提供缓存

这里面我们主要关注 reconciler 这个模块, packages/react-reconciler/src

├── react-reconciler ------------------------ reconciler相关代码│ ├── ReactFiberReconciler.js ------------- 模块入口├─ Model ----------------------------------------│ ├── ReactFiber.js ----------------------- Fiber相关│ ├── ReactUpdateQueue.js ----------------- state操作队列│ ├── ReactFiberRoot.js ------------------- RootFiber相关├─ Flow -----------------------------------------│ ├── ReactFiberScheduler.js -------------- 1.总体调度系统│ ├── ReactFiberBeginWork.js -------------- 2.Fiber解析调度│ ├── ReactFiberCompleteWork.js ----------- 3.创建DOM │ ├── ReactFiberCommitWork.js ------------- 4.DOM布局├─ Assist ---------------------------------------│ ├── ReactChildFiber.js ------------------ children转换成subFiber│ ├── ReactFiberTreeReflection.js --------- 检索Fiber│ ├── ReactFiberClassComponent.js --------- 组件生命周期│ ├── stateReactFiberExpirationTime.js ---- 调度器优先级│ ├── ReactTypeOfMode.js ------------------ Fiber mode type│ ├── ReactFiberHostConfig.js ------------- 调度器调用渲染器入口

Fiber reconciler 优化思路

Fiber reconciler 使用了scheduling(调度)这一过程,
每次只做一个很小的任务,做完后能够“喘口气儿”,回到主线程看下有没有什么更高优先级的任务需要处理,如果有则先处理更高优先级的任务,没有则继续执行(cooperative
scheduling 合作式调度)。

网友测试使用React V16,当DOM节点数量达到100000时,
页面能正常加载,输入交互也正常了;详情

所以Fiber 架构就是用 异步的方式解决旧版本 同步递归导致的性能问题。

Fiber 核心算法

编程最重要的是思想而不是代码,本段主要理清Fiber架构内核算法的编码思路;

Fiber 源码解析

之前一个师弟问我关于Fiber的小问题:Fiber 框架是否会自动给 Fiber
Node打上优先级?如果给Fiber Node打上的是async,
是否会给给它设置expirationTime?带着以上问题看源码,
结论:框架给每个 Fiber Node 打上优先级, 不管是sync 还是 async都会给
该Fiber Node 设置expirationTime, expirationTime 越小优先级越高。

个人阅读源码细节就不放了, 因为发现网上有更系统的Fiber
源码文章,虽然官方源码已更新至Flow语法, 但算法并没太大改变:React
Fiber源码分析 React Fiber源码分析 第一篇React Fiber源码分析 第二篇React
Fiber源码分析 第三篇

优先级

module.exports = { NoWork: 0, // No work is pending. SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects. AnimationPriority: 2, // Needs to complete before the next frame. HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive. LowPriority: 4, // Data fetching, or result from updating stores. OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.};

React Fiber 每个工作单元运行时有6种优先级:synchronous 与之前的Stack
reconciler操作一样,同步执行task 在next tick之前执行animation
下一帧之前执行high 在不久的将来立即执行low 稍微延迟执行也没关系offscreen
下一次render时或scroll时才执行

生命周期

生命周期函数也被分为2个阶段了:

// 第1阶段 render/reconciliationcomponentWillMountcomponentWillReceivePropsshouldComponentUpdatecomponentWillUpdate// 第2阶段 commitcomponentDidMountcomponentDidUpdatecomponentWillUnmount

第1阶段的生命周期函数可能会被多次调用,默认以low优先级
执行,被高优先级任务打断的话,稍后重新执行。

Fiber 架构对React开发影响

本段主要探讨React V16
后Fiber架构对我们使用React业务编程的影响有哪些?实际编码需要注意哪些内容。

1.不使用官方宣布弃用的生命周期。

为了兼容旧代码,官方并没有立即在V16版本废弃三生命周期,
用新的名字(带上UNSAFE)还是能使用。
建议使用了V16+版本的React后就不要再使用废弃的三生命周期。因为React
17版本将真正废弃这三生命周期:

到目前为止(React 16.4),React的渲染机制遵循同步渲染:1) 首次渲染:
willMount render didMount,2) props更新时: receiveProps shouldUpdate
willUpdate render didUpdate3) state更新时: shouldUpdate willUpdate
render didUpdate3) 卸载时:
willUnmount期间每个周期函数各司其职,输入输出都是可预测,一路下来很顺畅。BUT
从React 17 开始,渲染机制将会发生颠覆性改变,这个新方式就是 Async
Render。首先,async
render不是那种服务端渲染,比如发异步请求到后台返回newState甚至新的html,这里的async
render还是限制在React作为一个View框架的View层本身。通过进一步观察可以发现,预废弃的三个生命周期函数都发生在虚拟dom的构建期间,也就是render之前。在将来的React
17中,在dom真正render之前,React中的调度机制可能会不定期的去查看有没有更高优先级的任务,如果有,就打断当前的周期执行函数(哪怕已经执行了一半),等高优先级任务完成,再回来重新执行之前被打断的周期函数。这种新机制对现存周期函数的影响就是它们的调用时机变的复杂而不可预测,这也就是为什么”UNSAFE”。作者:辰辰沉沉大辰沉来源:CSDN

2.注意Fiber 优先级导致的bug;

了解Fiber原理后,
业务开发注意高优先级任务频率,避免出现低优先级任务延迟太久执行或永不执行bug。

3.业务逻辑实现别太依赖生命周期钩子函数;

在Fiber架构中,task
有可能被打断,需要重新执行,某些依赖生命周期实现的业务逻辑可能会受到影响。

原文:

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图