理解React Fiber
date
Nov 18, 2021
slug
react-fiber
status
Published
tags
summary
type
Post
Fiber的作用
- 在非Fiber模式下,一旦开始渲染一次更新,无法将这次更新在时间上划分更细的粒度了。在某些极端情况,一次更新就会造成阻塞和卡顿
- 阻塞会影响用户输入和动画,使用防抖会牺牲一定的响应及时性,使用节流会减少更新频率,都不是最佳的用户体验
- 引入Fiber协调器后的Concurrent模式支持以下特性
- 渲染的过程可以中断
- 可控的加载顺序,可以在就屏幕上多停留一段时间,跳过一些不重要的加载状态,再展示新的屏幕
- 可控的更新优先级
- 对于CPU计算型的更新,比如创建节点或者更新状态,任何一个更紧迫的更新都可以中断已经开始的渲染进行插队
- 对于IO型的更新,比如通过网络请求数据,在数据到达前在内存中渲染,避免不需要的加载状态
- 对于屏幕之外的更新,可以延迟相关逻辑
Fiber的实现
- 在引入Fiber前,React更新采用DFS的策略,DFS递归时隐式的用到了操作系统内部的堆栈来维护递归程序的调用过程,这一部分堆栈同样在内存中,但无法被React程序维护和访问到。
- React Fiber将DFS中隐式用到堆栈转移到React自己来声明和维护,通过Fiber中维护的信息,可以做到 中断更新过程 + 添加优先级更高的更新 + 继续更新过程
- 现代浏览器提供的api
- requestIdleCallback:调度一个低优先级函数在空闲时执行
- requestAnimationFrame:调度一个高优先级函数在下一个动画帧执行
有了上面两个api和一个自己维护的代替调用栈的信息,就可以做到自定义优先级,暂停和恢复渲染
具体Fiber List实现如下,假设有如下的组件树:

Fiber的节点修改三个指向,变成了如下的结构
- child指向第一个子节点(不再维护多个children的节点
- sibling指向第一个兄弟节点
- return指向父节点

在有了以上Fiber维护的数据结构后,可以在上面跑这样的DFS。优先遍历child,如果不存在子节点则遍历sibling,如果都不存在则返回父节点。
那么会有这样的遍历结果
a1 ⇒ b1 ⇒ b2 ⇒ c1 ⇒ d1 ⇒d2 ⇒ return c1
a1 ⇒ b1 ⇒ b2 ⇒ c1 ⇒ return b2
a1 ⇒ b1 ⇒ b2 ⇒ b3 ⇒ c2 ⇒ return b3
a1 ⇒ b1 ⇒ b2 ⇒ b3 ⇒ return a1
这种搜索方法算是DFS和BFS的共同使用,DFS的最深层级被限制到了和DOM树的高度相同,链表的最长长度也被限制到了(DOM树的高度+DOM树的宽度)
除了组成链表的sibling和child之外,Fiber节点还有一些React业务相关的字段,还没弄懂
export type Fiber = {
tag: TypeOfWork,
key: null | string,
type: any,
return: Fiber | null,
child: Fiber | null,
sibling: Fiber | null,
effectTag: TypeOfSideEffect,
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
alternate: Fiber | null,
stateNode: any,
...
}Fiber 异步渲染的阶段和生命周期
render / reconciliation阶段,此时可以中断
- 遍历Fiber Tree的每一个FiberNode,自顶向下构造workInProcess Tree
- 判断节点是否更新,需要的话则标记节点
- 调用SCU是否需要更新,需要的话调用render,返回该节点并创建对应FiberNode
- 当前FiberNode结束,返回effect列表
- 检查上一个Fiber的child或者sibling,作为下一个工作单元
- 检查剩余工作事件,有的话继续遍历
- 没有的话使用requestIdleCallback中,等待空闲时在遍历
- 直到遍历完所有节点,进入pendingCommit阶段
Commit阶段,执行副作用列表,此时不可中断
- 更新DOM树和ref指向
- 调用生命周期函数(CDM,CDU,CWU)和useEffect中的副作用

