面试官:用简单的话说清楚 Fiber

202 阅读5分钟

React v15 的困境

要聊 fiber 这个东西,时间需要稍稍往前回退几年,回到 react 尚未发布 v16 的时候。如果渲染一个极为复杂的dom,如一个大数据图表,react 的表现相当不如人意。当它加载时,页面几乎动不了,用户没法进行任何操作。

这是为什么呢?让我们来看一下 react 过去的渲染原理:

  1. 写好 jsx
  2. 将 jsx render 成虚拟节点树
  3. 数据变更 => 视图需要发生对应变化
  4. 构建新虚拟节点树,并和旧节点树进行递归对照,找到他们不同的地方,然后更新他们

这个过程也称之为协调(Reconcilation)

协调期间,react 将会抢走浏览器资源,当数据量大的时候,会使其卡顿很久,无法响应其他(诸如用户点击、输入)的操作。有什么办法解决这个问题呢?

它山之石,可以攻玉

这个听起来很像操作系统课上学过的进程调度问题,我们先简单回顾一下当时有哪些算法

先来先服务:哪个程序先到,哪个程序先执行。这个是最简单的默认方案,但是如果长进程多的话,对短进程十分不公平,要等待许久。

最短作业优先:哪个程序执行的快,哪个程序先。不错,但是如果一直有短进程,长进程将永无出头之日,对长进程不公平。

高响应比优先:响应比 =(等待时间 + 执行时间)/ 执行时间,由于添加了等待时间的参数,长进程有出头之日了,还不错。

时间片轮转:这个就更高级了,在进程可中断的背景下,每个进程都给一个固定的时间片,用完了就换人,这样对大家都友好,不会出现一个程序停滞半天不动的情况。

再回到我们刚才的问题,资源全被长进程(协调)抢占了,有什么好办法呢?

是的,时间片轮转

采用这个方案,资源会轮流交到 react 和用户手上,只要切换的够快,用户就不会感知到卡顿。

这有点类似两个顾客同时叫同一个服务员的情况,虽然他没办法分身,但是如果他以极快的速度反复横跳,飞到这说一句飞到那说一句,就可以达成同时服务两个顾客的效果。

不过时间片轮转是有个前提的:可中断

React 的破局之法

两个思路

过去 react 的协调无法中断,必须得等协调处理完毕以后,才能处理其他事务,所以 react 痛定思痛,做出了如下两个改动:

  1. 用更好的可中断的方式进行协调,也就是 Fiber 方案。

  2. 将协调拆分为协调 + 提交,从而更加精细化

浏览器的工作机制

先不谈 fiber 这个东西,我们只需要知道它可中断可恢复即可。先来想想,浏览器是否能支持中断自己的任务呢?答案是肯定的。

浏览器每一帧都要做许多事情:处理输入、处理js、回流、重绘等等。

在一帧中,当它完成了需要做的事,并且还有剩余时间时,就会调用 requestIdleCallback 的回调函数,fiber 正是利用了这个特性,将自己分片放入 requestIdleCallback 中,来实现时间片轮转。

注意:fiber只是利用了 requestIdleCallback 这个思路,但该 api 并不是对所有浏览器都支持的,所以 react 基于 MessageChannel 这个 api 来进行了实现

Fiber 是什么

fiber 被翻译为协程、纤程,它可以被分割打断。

你可以将其视为一个执行单元,每执行完一个执行单元,就会通过 requestIdleCallback 来检查还有多少剩余时间,没时间就把控制权让出给浏览器。

我们来看看 fiber 的数据结构

export type Fiber = {  
    type: any,  
    // ...  
    
    // 父节点
    return: Fiber | null,  
    // 子节点  
    child: Fiber | null, 
    // 下一个兄弟节点  
    sibling: Fiber | null,
}

这是一个很清晰的链状结构,有一张不错的图来说明:

当程序处理完当前 fiber 节点时,如果还有剩余时间,那么就处理下一个子节点,如果没有,就找兄弟节点,否则就回溯到父节点。

<root>
    <parent>
        <child1>some1</child1>
        <child2>some2</child2>
    </parent>
</root>

// 详细路径
root => parent => child1 => some1 => child1 => child2 => some2 => child2 => parent => root
// 去掉重复
root => parent => child1 => some1 => child2 => some2

因为使用了链结构,所以随时可以中断和恢复。

阶段拆分

协调阶段:大致可以认为是 diff 阶段,这个阶段将把变更收集起来,但不直接更新。这个阶段是可以打断的。

提交阶段:将所有变更一次性执行,不可打断

Diff 算法

再来简单说说这个 diff 算法吧,diff 算法的目的就是找出那些要新增、删除、修改、复用的部分,以减少直接更新的开销。算法分为两个阶段:

一阶段:按顺序一一对比所有节点,如果能复用,就打上复用标签,然后对照下一个,直到遇到不可复用的为止

二阶段:将不可复用的这些 fiber 节点,放入一个map中,跟剩下的 vdom 进行对比,如果有能复用的,则打上复用标签。最后把这些剩余的 fiber 节点打上删除标签,剩余没有对应的 vdom 节点打上新增标签。

想具体了解 diff 算法的同学,建议看我的另一篇文章 别慌,react diff 没有那么复杂

总结

Fiber 其实就是一种链状数据结构的基础单元。

在过去,react 使用直接递归渲染 vdom 的方案,此方案不可中断,会有卡顿问题。

所以增加了一步:将 vdom 转化为 fiber 再进行操作,这种链式处理在收集 effect 的过程中随时可中断,最后再将这些 effect 一次性进行更新(这个commit 过程不可中断)