React Fiber

fiber

react在进行组件渲染时,从setState开始到渲染完成整个过程是同步的(“一鼓作气”)。若是须要渲染的组件比较庞大,js执行会占据主线程时间较长,会致使页面响应度变差,使得react在动画、手势等应用中效果比较差。html

为了解决这个问题,react团队通过两年的工做,重写了react中核心算法——reconciliation。并在v16版本中发布了这个新的特性。为了区别以前和以后的reconciler,一般将以前的reconciler称为stack reconciler,重写后的称为fiber reconciler,简称为Fiber。react

卡顿缘由

Stack reconciler的工做流程很像函数的调用过程。父组件里调子组件,能够类比为函数的递归(这也是为何被称为stack reconciler的缘由)。在setState后,react会当即开始reconciliation过程,从父节点(Virtual DOM)开始遍历,以找出不一样。将全部的Virtual DOM遍历完成后,reconciler才能给出当前须要修改真实DOM的信息,并传递给renderer,进行渲染,而后屏幕上才会显示这次更新内容。对于特别庞大的vDOM树来讲,reconciliation过程会很长(x00ms),在这期间,主线程是被js占用的,所以任何交互、布局、渲染都会中止,给用户的感受就是页面被卡住了。算法

Scheduler

scheduling(调度)是fiber reconciliation的一个过程,主要决定应该在什么时候作什么。👆的过程代表在stack reconciler中,reconciliation是“一鼓作气”,对于函数来讲,这没什么问题,由于咱们只想要函数的运行结果,但对于UI来讲还须要考虑如下问题:浏览器

  • 并非全部的state更新都须要当即显示出来,好比屏幕以外的部分的更新
  • 并非全部的更新优先级都是同样的,好比用户输入的响应优先级要比经过请求填充内容的响应优先级更高
  • 理想状况下,对于某些高优先级的操做,应该是能够打断低优先级的操做执行的,好比用户输入时,页面的某个评论还在reconciliation,应该优先响应用户输入

因此理想情况下reconciliation的过程应该是像下图所示同样,每次只作一个很小的任务,作完后可以“喘口气儿”,回到主线程看下有没有什么更高优先级的任务须要处理,若是又则先处理更高优先级的任务,没有则继续执行(cooperative scheduling 合做式调度)。markdown

任务拆分 fiber-tree & fiber

先看一下stack-reconciler下的react是怎么工做的。代码中建立(或更新)一些元素,react会根据这些元素建立(或更新)Virtual DOM,而后react根据更新先后virtual DOM的区别,去修改真正的DOM。注意,在stack reconciler下,DOM的更新是同步的,也就是说,在virtual DOM的比对过程当中,发现一个instance有更新,会当即执行DOM操做ide

而fiber-conciler下,操做是能够分红不少小部分,而且能够被中断的,因此同步操做DOM可能会致使fiber-tree与实际DOM的不一样步。对于每一个节点来讲,其不光存储了对应元素的基本信息,还要保存一些用于任务调度的信息。所以,fiber仅仅是一个对象,表征reconciliation阶段所能拆分的最小工做单元,和上图中的react instance一一对应。经过stateNode属性管理Instance自身的特性。经过child和sibling表征当前工做单元的下一个工做单元,return表示处理完成后返回结果所要合并的目标,一般指向父节点。整个结构是一个链表树。每一个工做单元(fiber)执行完成后,都会查看是否还继续拥有主线程时间片,若是有继续下一个,若是没有则先处理其余高优先级事务,等主线程空闲下来继续执行。函数

fiber {
  	stateNode: {},
    child: {},
    return: {},
    sibling: {},
}
复制代码

举个例子

当前页面包含一个列表,经过该列表渲染出一个button和一组Item,Item中包含一个div,其中的内容为数字。经过点击button,可使列表中的全部数字进行平方。另外有一个按钮,点击能够调节字体大小。oop

页面渲染完成后,就会初始化生成一个fiber-tree。初始化fiber-tree和初始化Virtual DOM tree没什么区别,这里就再也不赘述。布局

于此同时,react还会维护一个workInProgressTree。workInProgressTree用于计算更新,完成reconciliation过程。字体

用户点击平方按钮后,利用各个元素平方后的list调用setState,react会把当前的更新送入list组件对应的update queue中。可是react并不会当即执行对比并修改DOM的操做。而是交给scheduler去处理。

scheduler会根据当前主线程的使用状况去处理此次update。为了实现这种特性,使用了requestIdelCallbackAPI。对于不支持这个API的浏览器,react会加上pollyfill。

总的来说,一般,客户端线程执行任务时会以帧的形式划分,大部分设备控制在30-60帧是不会影响用户体验;在两个执行帧之间,主线程一般会有一小段空闲时间,requestIdleCallback能够在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务

  1. 低优先级任务由requestIdleCallback处理;
  2. 高优先级任务,如动画相关的由requestAnimationFrame处理;
  3. requestIdleCallback能够在多个空闲期调用空闲期回调,执行任务;
  4. requestIdleCallback方法提供deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而致使掉帧;

一旦reconciliation过程获得时间片,就开始进入work loop。work loop机制可让react在计算状态和等待状态之间进行切换。为了达到这个目的,对于每一个loop而言,须要追踪两个东西:下一个工做单元(下一个待处理的fiber);当前还能占用主线程的时间。第一个loop,下一个待处理单元为根节点。

由于根节点上的更新队列为空,因此直接从fiber-tree上将根节点复制到workInProgressTree中去。根节点中包含指向子节点(List)的指针。

根节点没有什么更新操做,根据其child指针,接下来把List节点及其对应的update queue也复制到workinprogress中。List插入后,向其父节点返回,标志根节点的处理完成。

根节点处理完成后,react此时检查时间片是否用完。若是没有用完,根据其保存的下个工做单元的信息开始处理下一个节点List。

接下来进入处理List的work loop,List中包含更新,所以此时react会调用setState时传入的updater funciton获取最新的state值,此时应该是[1,4,9]。一般咱们如今在调用setState传入的是一个对象,但在使用fiber conciler时,必须传入一个函数,函数的返回值是要更新的state。react从很早的版本就开始支持这种写法了,不过一般没有人用。在以后的react版本中,可能会废弃直接传入对象的写法。

setState({}, callback); // stack conciler
setState(() => { return {} }, callback); // fiber conciler
复制代码

在获取到最新的state值后,react会更新List的state和props值,而后调用render,而后获得一组经过更新后的list值生成的elements。react会根据生成elements的类型,来决定fiber是否可重用。对于当前状况来讲,新生成的elments类型并无变(依然是Button和Item),因此react会直接从fiber-tree中复制这些elements对应的fiber到workInProgress 中。并给List打上标签,由于这是一个须要更新的节点。

List节点处理完成,react仍然会检查当前时间片是否够用。若是够用则处理下一个,也就是button。加入这个时候,用户点击了放大字体的按钮。这个放大字体的操做,纯粹由js实现,跟react无关。可是操做并不能当即生效,由于react的时间片还未用完,所以接下来仍然要继续处理button。

button没有任何子节点,因此此时能够返回,并标志button处理完成。若是button有改变,须要打上tag,可是当前状况没有,只须要标记完成便可。

老规矩,处理完一个节点先看时间够不够用。注意这里放大字体的操做已经在等候释放主线程了。

接下来处理第一个item。经过shouldComponentUpdate钩子能够根据传入的props判断其是否须要改变。对于第一个Item而言,更改先后都是1,因此不会改变,shouldComponentUpdate返回false,复制div,处理完成,检查时间,若是还有时间进入第二个Item。

第二个Item shouldComponentUpdate返回true,因此须要打上tag,标志须要更新,复制div,调用render,讲div中的内容从2更新为4,由于div有更新,因此标记div。当前节点处理完成。

对于上面这种状况,div已是叶子节点,且没有任何兄弟节点,且其值已经更新,这时候,须要将此节点改变产生的effect合并到父节点中。此时react会维护一个列表,其中记录全部产生effect的元素。

合并后,回到父节点Item,父节点标记完成。

下一个工做单元是Item,在进入Item以前,检查时间。但这个时候时间用完了。此时react必须交换主线程,并告诉主线程之后要为其分配时间以完成剩下的操做。

主线程接下来进行放大字体的操做。完成后执行react接下来的操做,跟上一个Item的处理流程几乎同样,处理完成后整个fiber-tree和workInProgress以下:

完成后,Item向List返回并merge effect,effect List如今以下所示:

此时List向根节点返回并merge effect,全部节点均可以标记完成了。此时react将workInProgress标记为pendingCommit。意思是能够进入commit阶段了。

此时,要作的是仍是检查时间够不够用,若是没有时间,会等到时间再去提交修改到DOM。进入到阶段2后,reacDOM会根据阶段1计算出来的effect-list来更新DOM。

更新完DOM以后,workInProgress就彻底和DOM保持一致了,为了让当前的fiber-tree和DOM保持一直,react交换了current和workinProgress两个指针。

事实上,react大部分时间都在维持两个树(Double-buffering)。这能够缩减下次更新时,分配内存、垃圾清理的时间。commit完成后,执行componentDidMount函数。

小结

经过将reconciliation过程,分解成小的工做单元的方式,可让页面对于浏览器事件的响应更加及时。可是另一个问题仍是没有解决,就是若是当前在处理的react渲染耗时较长,仍然会阻塞后面的react渲染。这就是为何fiber reconciler增长了优先级策略。

优先级

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.
};
复制代码

优先级策略的核心是,在reconciliation阶段,低优先级的操做能够被高优先级的操做打断,并让主线程执行高优先级的更新,以时用户可感知的响应更快。值得注意的一点是,当主线程从新分配给低优先级的操做时,并不会从上次工做的状态开始,而是重新开始。

这就可能会产生两个问题:

  • 饿死:正在实验中的方案是重用,也就是说高优先级的操做若是没有修改低优先级操做已经完成的节点,那么这部分工做是能够重用的。
  • 一次渲染可能会调用屡次声明周期函数

生命周期函数

对于某些状况来讲,phase1阶段的生命周期函数可能会不止执行一次。好比说,当一个低优先级的componentWillUpdate执行以后,被高优先级的打断,高优先级执行完以后,再回到低优先级的操做中来,componentWillUpdate可能会再执行一次。对于某些只指望执行一次,或者须要在两个生命周期函数的操做中执行对称操做的状况而言,要考虑这种case,确保不会让整个App crash掉。

参考

Reconciliation

Fiber reconciler

cooperative scheduling 合做式调度

Lin Clark presentation in ReactConf 2017

相关文章
相关标签/搜索