什么是 React Fiber
React 16 起开始使用的 reconciler
新特性
- 将 reconciliation 变成可中断的工作方式
- React 可以可以按照不同更新任务的优先级来安排工作
解决了什么问题
之前不可中断的更新方式容易导致 JS 引擎运行时间过长,导致页面渲染流程被阻塞。 因为 React 16 使用的是 Stack Reconciler, 使用递归的方式遍历 Virtual DOM,整个工作在遍历完之前无法中断
实现细节
引入 fiber 节点
fiber 节点时一种 React 中的数据结构,每个 fiber 对应每个单独的 React Element, Virtual DOM 上的一个节点
_18type Fiber = {_18 // 当前 Fiber 处理完成后返回的 Fiber 节点_18 // 也就是父亲节点_18 return: Fiber | null,_18_18 // 执行当前的节点的孩子节点和兄弟节点_18 // 因此 Fiber 兄弟之间、父子之间形成了链表_18 child: Fiber | null,_18 sibling: Fiber | null,_18_18 // 与 Fiber 相关联的 React 实例_18 stateNode: any_18_18 // 指向 effect list 链表中的下一个具有 side effect 的 Fiber_18 nextEffect: Fiber | null,_18_18 // ..._18}
可以看到单个 fiber 包含指向 child
, parent
, sibling
的指针,因此可以理解为添加了链表的树,或图
Fiber 的遍历方式
TL;DR 非递归的 DFS
由于每个 fiber 包含上述相关 fiber 的指针,因此每个 fiber 处理结束后可以返回下一个需要处理的 fiber 是什么。这为 Fiber 树更新阶段的可中断特性提供基础。程序不再需要依靠调用栈来跟踪它在树中的位置以供回溯 ( 递归遍历树的原理 )
页面初始渲染时期
DFS 遍历为每个 React element 创建 fiber,形成 Fiber Tree
页面更新时期
与之前不同的是,对于每一个更新任务,React Fiber 将更新过程分为两个阶段:渲染阶段和提交 commit 阶段
渲染阶段 ( reconcilation )
- 可中断
Fiber 对树进行遍历,并根据需要做的更新生成新的 wip ( work-in-progress ) 树。树的遍历与二叉树的先序遍历很像
- 从根节点开始遍历
- 处理当前节点
- 如果有数据结构中的
child
属性判断是否存在孩子节点,有则跳转都孩子节点,并重复 2 - 如果已经是叶子节点了,则判断是否存在兄弟节点,有则跳转到兄弟节点,并重复 2
- 如果没有未访问的兄弟节点和孩子节点了,则宣布该节点已完成,返回父亲节点
在处理节点的过程中,如果该节点需要更新,则对该节点打上标记。
当节点为已完成状态时,如果它身上有标记,则将它添加到名为 effect list
的链表中,等待 commit 阶段统一更新。
commit 阶段
- 不可中断
在这一阶段,React 首先按照 effect list
中的顺序将所有变更更新到 DOM 上,并执行相应的生命周期函数 ( 对于 class component 是这样 ) 。
总结以上两个阶段,对于一个更新任务来说,我们可以这样理解:
一个任务可分为 fiber1|fiber2|fiber3|...|fiberX| ( commit ) 这样的工作单元序列。
在所有 |
的地方,都是可中断的 ( 注意:不是一定会中断 ) 。
分配优先级
页面运行过程中的更新任务被依次加入在 update queue
中。React 中每一种任务有不同的优先级:
- Synchronous, 与 Stack Reconciliation 类似
- Task, 需要在下一个事件循环周期前完成
- Animation ( 动画 ) , 需要在下一帧重绘前完成
- High, 高优先级的任务可以插队到低优先级任务之前
- Low, 如远程数据请求,更新的略微延迟 ( 几百毫秒 ) 相比于之前的网络传输可以忽略不记
- Offscreen,对隐藏的元素或者暂时不在屏幕中的元素进行更新,为将来的显示做准备
如何调度
对于 High, Low, Offscreen 任务,React 会调用 requestIdleCallback
,告诉浏览器在每一帧的空闲时间执行。
拿低优先级的更新任务举例,每执行完一个工作单元 ( 也就是一个 fiber ) 后,调度器都会查看下一帧开始前的剩余时间。如果还有时间,则会执行下一个工作单元。此时假设有其他高优先级的任务出现,调度器并不会立即执行它,而是仍会执行当前任务,直到剩余时间用尽。
如果时间用尽前,当前任务仍然没有完成,那么 React 会在结束前继续调用 requestIdCallback
, 在下一个空闲时段继续剩下的工作
当剩余时间用尽后,调度器根据 update queue
中的认识的优先级选择更新任务执行。此前出现的高优先级任务此时会被优先执行。执行时,调度会丢弃进行到一半的低优先级任务的工作成果,并在高优先级任务结束后再重新执行原先的低等级任务。
更新
React已经不再使用requestIdleCallback
,而是使用自己的scheduler
总结
React Fiber 本质上就是使用非递归的方式更新 fiber 树,使得 React 不再为了遍历完整棵树而一直霸占 Main Thread。同时吸取了与显卡渲染中的双缓存机制类似的概念,在可中断的同时保证页面的一致性。
参考
An Introduction to React Fiber - The Algorithm Behind React
A Cartoon Intro To Fiber - React Conf 2017