React源码剖析——(四)新引擎React Fiber

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

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

1

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

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

2

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

3

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

4

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

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

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

新问题

之前,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 如下图所示:


5

Fiber

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

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

React 核心团队很早之前就预知这样的风险的存在,并且持续探索可解决的方式。基于浏览器对requestIdleCallbackrequestAnimationFrame这两个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,硬件,就会更新对应的元素。

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

Fiber1

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


Fiber2

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

5

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

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

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

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

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候选推荐标准

写在最后

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

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

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

……

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

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

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

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

……

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

参考资料:

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,请大家期待(~ ̄▽ ̄)~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 前言 浏览器中的渲染引擎是单线程的,几乎所有的操作都在这个单线程中执行——解析渲染DOM Tree和CSS Tre...
    ThoughtWorks阅读 7,254评论 3 31
  • 参考文章:深度剖析:如何实现一个Virtual DOM 算法 作者:戴嘉华React中一个没人能解释清楚的问题——...
    waka阅读 5,952评论 0 21
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,810评论 1 18
  • 30年了,你老了鼓锤还在你从旧鼓皮中把时间捡起缝缝补补,敲敲打打仔细端详,像是老照片里的初恋 黄河水已干,黄土风沙...
    mao眼阅读 367评论 0 1
  • 只是因为太年轻,所以所有的悲伤和快乐都显得那么深刻,轻轻一碰就惊天动地。
    毛线毛球阅读 151评论 0 0