React Fiber架构如何从JS引擎手中“夺回”调度权

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的学习专栏
分享对于计算机科学的学习和思考,只发布有价值的文章: 对于那些网上已经有完整资料,且相关资料已经整...

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...

1.1k 声望
181 粉丝
0 条评论
推荐阅读
手写一个Parser - 代码简单而功能强大的Pratt Parsing
在编译的流程中,一个很重要的步骤是语法分析(又称解析,Parsing)。解析器(Parser)负责将Token流转化为抽象语法树(AST)。这篇文章介绍一种Parser的实现算法:Pratt Parsing,又称Top Down Operator Precede...

csRyan阅读 2.7k

正则表达式实例
收集在业务中经常使用的正则表达式实例,方便以后进行查找,减少工作量。常用正则表达式实例1. 校验基本日期格式 {代码...} {代码...} 2. 校验密码强度密码的强度必须是包含大小写字母和数字的组合,不能使用特殊...

寒青57阅读 8.6k评论 11

JavaScript有用的代码片段和trick
平时工作过程中可以用到的实用代码集棉。判断对象否为空 {代码...} 浮点数取整 {代码...} 注意:前三种方法只适用于32个位整数,对于负数的处理上和Math.floor是不同的。 {代码...} 生成6位数字验证码 {代码...} ...

jenemy49阅读 7.3k评论 12

再也不学AJAX了!(二)使用AJAX ① XMLHttpRequest
「再也不学 AJAX 了」是一个以 AJAX 为主题的系列文章,希望读者通过阅读本系列文章,能够对 AJAX 技术有更加深入的认识和理解,从此能够再也不用专门学习 AJAX。本篇文章为该系列的第二篇,最近更新于 2023 年 1...

libinfs42阅读 6.9k评论 12

封面图
CSS 绘制一只思否猫
欢迎关注我的公众号:前端侦探练习 CSS 有一个比较有趣的方式,就是发挥想象,绘制各式各样的图案,比如来绘制一只思否猫?思否猫,SegmentFault 思否的吉祥物,是一只独一无二、特立独行、热爱自由的(&gt;^ω^&lt...

XboxYan47阅读 3.3k评论 14

封面图
「多图预警」完美实现一个@功能
一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala...(内心 OS:不听,讲重点)产品大大:咱们的客服 I...

wuwhs32阅读 3.5k评论 5

封面图
还在用 JS 做节流吗?CSS 也可以防止按钮重复点击
举个例子:一个保存按钮,为了避免重复提交或者服务器考虑,往往需要对点击行为做一定的限制,比如只允许每300ms提交一次,这时候我想大部分同学都会到网上直接拷贝一段throttle函数,或者直接引用lodash工具库

XboxYan35阅读 2.7k评论 2

封面图

So you're passionate? How passionate? What actions does your passion lead you to do? If the heart...

1.1k 声望
181 粉丝
宣传栏