title: react-fiber
date: 2017-06-28 12:04:32

tags:

react fiber

react 是一个非常流行的前端组件库,网络上已有非常多的学习资料,本文主要是介绍 react fiber 的算法,我们会先回顾一下 react 的原理,以了解现有 react 版本算法有哪些问题,再引出 firber 是怎么解决这些问题,让我们的 react 程序可以有更好的用户体验。

react

react 的核心思路就是通过 render 方法产生一棵内存树,树上的节点有发生变化则产生另一棵树,拿两棵树进行对比,找出有两棵树的差异(属性修改、元素增加、元素移动、元素删除),通过 native 提供的 api 更新这些差异到最终的 gui 平台上,如 browser、react native、wpf,如下流程:
[This is an example image]

v-dom diff vs patch

dom-diff-patch

图1 是第一次通过 ReactDom.render 出的一棵树,图2 是因为红色节点通过 setState 发生了状态变化,此时会从这棵节点开始重新调用 render 产生一棵新的子树,和第一次的内存树中同级别的子树进行 diff,diff 后发现子节点 a 和 b 的位置需要变换,react 会找出这个差异,把这一步 patch 到 dom-tree 上。

更多细节可参考 implementation notes

fiber 的目的

fiber 是在动画、手势等场景提供更友好的体验,可设置任务的优先级,通过把 render 折分成不同的块(chunk),以达到任务的暂停、取消,从而在碰到优先级高的任务可以暂停,让高优先级的任务先执行。

如下场景很需要 fiber:

  1. react 正在 render 下一帧的动画界面时,但此时有另一个任务,如服务端数据的到来,造成树的重新 render ,此帧执行时间较长,造成动画当前帧执行时间过长,从而产生掉帧。
  2. 正在处理一个长时间的重新 render,用户移动了滚动条操作,造成用户滚动延迟。

以上这些操作就需要设置优先级,如动画帧、用户滚动条操作要优先级更高。 fiber 提供了一个调度系统,在每个 chunk 完成后,会去检查一下当前是否有更高优先级的任务要处理,从而可以判断出哪个任务需要优选处理。

实现方案

我们先体验两个例子:

  1. react stack,react stack 的实现方案,也就是我们现在使用的 react 版本。
  2. react fiber,react fiber 的实现方案。

程序做的事情很简单:

  1. 三角形缩放的动画。
  2. 定时 1秒 更新三角形数字变化。
  3. 鼠标移动三角形变换颜色。

从上可以看出 react stack,基本没办法使用,卡的你想哭,看到这个版本感觉小心脏都受不了。而 fiber 这个流畅感觉好舒服。

react statck 里为什么会这么卡?
三角形的缩放动画是以 16 ms 去更新每帧的画面,每帧都要对每个树进行重新 render,这些操作可以在 16ms 里完成,可定时 1秒 执行的数字变化会落到动画的某一帧里执行,假如这个变换数字的操作需要 20 ms,那 render 三角形缩放的时间加上变化数字需要的时间,总共加起来需要超过了 16ms,因此这里就造成了掉帧并产生了卡顿。

react fiber 为什么这么流畅?
fiber 的处理其实很简单, fiber 会把数字变换的任务折成不同的 chunk,把每个 chunk 分布到各动画帧执行完后还留下的时间里去执行,每次执行 chunk 产生的 dom update 存在一个更新链表里,待所有 chunk 都执行完后,统一把链表里的更新更新到 dom 里,这样就不影响动画帧的 render。

什么是帧?
显示器显示的每个界面是按一定的频率切换显示,单位为 hz,浏览器传送到显示器的画面有一个协商机制 v-sync,协商好后,按这个频率把界面送给浏览器展示,如每 16ms 传送,当然浏览器也会根据每一帧执行的时间来调整,最终算出一个帧率时间。帧率能够达到 50~60 fps 的动画将会相当流畅,让人倍感舒适。

什么是任务?
某个操作造成的 v-dom 树重新 render,就为一次任务;如例子中,render 下一帧动画,定时任务 1s 中产生的数字变化都是一个任务。

任务的暂停、停止
如现在我们正在执行一个 1s 中产生的数字变化任务,此时浏览器需要下一帧的动画,fiber 会停止数字变化的任务,先执行动画需要的帧任务,原理是会把 v-dom 拆成不同的 chunk,每个 chunk 分别代表一个树上的节点,fiber 每执行完一个 chunk 都会检查当前是否有更高优先级的任务需要执行,如果有则停止本次任务的执行转而执行更高优先级的任务,如果没有更高优先级的则继续执行此任务。

fiber 中的任务调度介绍:

现在任务的暂停、停止有了实现方法,处理这些任务之间的协调操作,fiber 实现了一个任务调度,就像操作系统里的进程调度一样,主要有以下操作:

  1. 每个任务在执行完每个 chunk ,需要判断当前是否有优先级更高的任务需要执行。
  2. fiber 并不会在某一个任务暂停时记录执行到了哪个 chunk ,下一次恢复时,而是重新执行,所以这就为什么产生多次 componentWillUpdate 执行。

调度原理:

把所有任务设置为一个队列,每个任务就是 diff 一棵树,树的每个节点在 diff 完时,会把 diff 产生的差异存起来,检查当前是否有更高优先级的任务,如果有则执行更高优先级的,否则执行当前任务;循环以上的操作;如果此时又有一个树要更新,则把树的根节点存放在任务队列中。