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

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

动机:concurrent rendering

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

  • time slicing(分片)

    • 为了让浏览器保持60fps,因此渲染一帧需要在16.67ms内完成,否则会造成“卡顿”
    • time slicing将渲染工作切分,从而保证JavaScript的执行不会造成卡顿
    • 另一个功能是,将渲染工作按重要性来排序,提高时间敏感(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指针,有这三个指针的链表就能够实现深度优先遍历(其实scheduler还能够更加灵活地调度,使得react能够优先执行重要组件的渲染)。
  • fiber与调用栈的另一个区别是,栈帧在函数返回以后就销毁了,而fiber会在渲染结束以后继续存在,保存组件实例的信息。
React Fiber是使用JavaScript实现的,这意味着它的底层依然是JavaScript调用栈。

Fiber其实是计算机科学中早已存在的概念。Fiber的英文含义就是“纤维”,意指比Thread更细的线,寓意它是比线程(Thread)控制得更精密的执行模型。fiber是协作的(cooperatively)、可控的。一个fiber执行完自己的工作以后,会主动让出控制权,不会主宰(dominate)整个程序的执行。

协程(Coroutines)基本是相同的概念,它们的区别微乎其微。说白了,React Fiber就是用JavaScript重新实现了一个协程模型。
话说回来,generator函数也能够主动让出程序控制权(generator函数本质就是协程),用它也能够做到concurrent rendering。为什么react不使用generator函数而是重新实现协程,应该是因为后者能够更加灵活吧,比如generator函数不支持回到之前的yield状态,而fiber支持从任意一个fiber节点重新开始渲染。

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

摘自React Fiber是什么:

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,这时候,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会 完全作废,然后等待机会重头再来
因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段(Phase):第一个阶段Reconciliation Phase和第二阶段Commit Phase。
在第一阶段Reconciliation Phase,React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的;但是到了第二阶段Commit Phase,那就一鼓作气把DOM更新完,绝不会被打断。

生命周期示意图:

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

time slicing(分片)

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

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

Suspense

如果渲染到某个组件时,发现渲染需要暂停(比如需要等待React.lazy组件的加载,我们假设组件层级为 -> -> ),那么在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

你可能感兴趣的:(react.js,web,前端,前端框架)