17

React 16采用新的Fiber架构对React进行完全重写,同时保持向后兼容。

动机:concurrent rendering

concurrent rendering 又叫 async rendering,主要包含2个特性:

  • time slicing(分片)

    • 为了让浏览器保持60fps,渲染一帧需要在16.67ms内完成,否则会造成“卡顿”
    • time slicing将渲染工作切分,保证JavaScript的执行不会造成卡顿:如果当前帧的时间片已经用完,React就将控制权交还给浏览器,剩下的工作等到下一帧再做
    • 另一个功能是,将渲染工作按重要性来排序,提高时间敏感(time-sensitive)渲染的优先级(比如text input)
  • suspense

    • 让任何一个组件能够暂停渲染,等待数据的获取(比如懒加载组件、比如网络请求数据)

这两个特性的关键前提是:React的渲染能够被中止(interrupt)、恢复。
这就是为什么我们需要fiber架构了。

背景:JavaScript的执行模型:call stack

首先,我们先解释,为什么过去的架构无法支持渲染中止。

JavaScript原生的执行模型:通过调用栈来管理函数执行状态。

其中每个栈帧表示一个工作单元(a unit of work),存储了函数调用的返回指针、当前函数、调用参数、局部变量等信息。
因为JavaScript的执行栈是由引擎管理的,执行栈一旦开始,就会一直执行,直到执行栈清空。无法按需中止。

这与React有什么关系呢?React将视图看做函数调用的结果:

View = Component(Data)

Component会递归调用其他的Component。页面复杂的话,这个调用栈会很深,导致UI变卡。
在React Fiber之前,React的渲染就是使用原生执行栈来管理组件树的递归渲染。这意味着,整颗组件树的渲染必须一次性完成,工作无法被分片。
因此,react需要另一种可控的执行模型,让react来管理工作的调度。

React Fiber架构:可控的“调用栈”

React Fiber架构就是用JavaScript来实现的执行模型。可以将它比作由react管理的“调用栈”,一个fiber与一个函数栈帧非常类似,它们都表示一个工作单元(a unit of work)。一个组件实例对应一个Fiber。

函数栈帧 fiber
返回指针 父组件
当前函数 当前组件
调用参数 props
局部变量 state

React Fiber的构造函数源码

React Fiber与调用栈的区别:

  • React Fiber是链表结构,过去的递归调用变成了对fiber的链表遍历。fiber不仅有return指针,还有child、sibling指针,有这三个指针的链表就能够实现深度优先遍历。
  • fiber与调用栈的另一个区别是,栈帧在函数返回以后就销毁了,而fiber会在渲染结束以后继续存在,保存组件实例的信息(比如state)。
React Fiber是使用JavaScript实现的,这意味着它的底层依然是JavaScript调用栈。

Fiber其实是计算机科学中早已存在的概念。Fiber的英文含义就是“纤维”,意指比Thread更细的线,寓意它是比线程(Thread)控制得更精密的执行模型。在广义的计算机科学概念中,Fiber是一种协作的(cooperative)编程模型,帮助开发者用一种【既模块化又协作化】的方式来编排代码。一个fiber执行完自己的工作以后,会主动让出控制权,不会主宰(dominate)整个程序的执行。

协程(Coroutines)基本是相同的概念,它们的区别微乎其微。说白了,React Fiber就是用JavaScript实现的一种协程模型。

React Fiber就是React自己实现的底层执行模型。得益于自主掌控的底层执行模型,React能够在这个执行模型之上实现更多的编程模式,而不必受到过多来自JavaScript语言的约束:

  • 随时暂停、恢复渲染
  • 进行并发渲染:一个渲染还没有完成就开始另一个渲染,并控制各个渲染的优先级
  • componentDidCatch
  • Suspense
  • Algebraic Effects,以及基于它的React hooks编程模式,Algebraic Effects为一个组件(一个纯函数)提供了一个运行时的上下文:组件树中的一个组件实例
React团队经常将React比作一种“语言”,这些编程模式就是它的“语言特性”。(1), (2)

与Fiber相反,JS原生调用栈则不可控、不协作(non-cooperative)。如果函数不断地递归调用,那么它就会主宰整个程序,后续的工作(比如浏览器paint)必须等待它执行完成。

为什么不使用generator来实现协作式调度

generator函数也能够主动让出程序控制权(generator函数本质就是协程),理论上用它也能够实现concurrent rendering。那为什么react不使用generator函数而是自己实现Fiber呢?

一方面,如果React全面使用generator,那么React内部的调度逻辑、用户编写的所有组件都是generator,这会给用户增加心智负担,并且大量使用generator会有不小的性能开销,过于依赖执行引擎的优化;
另一方面,Fiber架构能够更加灵活地让React从任意一个Fiber恢复执行(不只是从上次中断的地方恢复,而且能够从更早的Fiber恢复),而generator函数只能回到之前的yield状态,不能回到更早的执行状态。

引用资料

详见知乎讨论

只有Render Phase是可以打断的

React Fiber的更新过程被分为两个阶段(Phase):第一阶段Render Phase;第二阶段Commit Phase。

在第一阶段Render Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的,甚至可以中途放弃;但是到了第二阶段Commit Phase,就必须一鼓作气把DOM更新完,绝不会被打断。

生命周期示意图

clipboard.png

Fiber架构如何满足前述的“动机”

time slicing(分片)

当一个Fiber的工作执行完,控制权会交还给React Scheduler,后者会检查【渲染一帧的可用时间】是否已经用完:

  • 如果还有足够的时间,那么React Scheduler会将控制权交给下一个Fiber。
  • 如果时间不足,那么React Scheduler会通过requestIdleCallback让浏览器在空闲的时候唤醒自己,然后将控制权交还给浏览器(执行栈清空即浏览器获得控制权)。

Suspense

如果渲染到某个组件时,发现渲染需要暂停(比如需要等待React.lazy组件的加载,我们假设组件层级为<App> -> <User> -> <LazyComponent>),那么在User组件的渲染函数中,会抛出一个Promise。得益于React Fiber架构,调用栈并不是React scheduler -> App -> User,而是:先React scheduler -> App然后React scheduler -> User。因此User组件抛出的错误会被React scheduler接住,React scheduler会将渲染“暂停”在User组件。这意味着,App组件的工作不会丢失。等到promise解析到数据以后,从User fiber开始重新渲染就好了(相当于控制权直接交还给User)。

Algebraic Effects,以及它在React中的应用讨论了它背后的理论概念。

参考资料

Algebraic effects, Fibers, Coroutines...
React Fiber Architecture
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
Fiber Principles: Contributing To Fiber
如何看待 Crank 这个前端框架? - csr632的回答 - 知乎

<!--Continuations, coroutines, fibers, effects-->


csRyan
1.1k 声望198 粉丝

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart doesn't find a perfect rhyme with the head, then your passion means nothing.