什么是 Fiber ?
React 新一代调和(Reconcilation)引擎,同时也是一种数据结构
React 的 虚拟 dom,最小粒度的执行单元,可以设置不同的优先级
Fiber 保存了哪些信息?
1 | function FiberNode(){ |
element、fiber、dom三种什么关系?
element
React 视图层在代码层级上的表象
开发者写的 jsx 语法,写的元素结构,都会被创建成 element 对象的形式
上面保存了 props , children 等信息
dom
- 元素在浏览器上给用户直观的表象
fiber
element 和真实 DOM 之间的交流枢纽站
一方面每一个类型 element 都会有一个与之对应的 fiber 类型,element 变化引起更新流程都是通过 fiber 层面做一次调和改变,然后对于元素,形成新的 DOM 做视图渲染
element 与 fiber 之间的对应关系
1 | export const FunctionComponent = 0; // 对应函数组件 |
Fiber 架构解决了什么问题?
主要解决的是同步递归渲染造成的页面卡顿问题
在 React v15 以及之前的版本,React 对于虚拟 DOM 是采用递归方式遍历更新的,递归一旦开始,中途无法中断
每一个 fiber 可以根据自身的过期时间expirationTime( v17 版本叫做优先级 lane )来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘 repaints 之类的事情,这样就能给用户感觉不是很卡
然后等浏览器空余时间,在通过 scheduler (调度器),再次恢复执行单元上来
Fiber root 和 root fiber 有什么区别?
fiberRoot
:首次构建应用, 创建一个 fiberRoot ,作为整个 React 应用的根基
rootFiber
:通过 ReactDOM.render 渲染出来的
1 | ReactDOM.render(<Index/>, document.getElementById('app')) |
一个 React 应用可以有多 ReactDOM.render 创建的 rootFiber ,但是只能有一个 fiberRoot(应用根节点)
不同fiber 之间如何建立起关联的?
每一个 fiber 是通过以下三个属性建立起联系的
return: 指向父级 Fiber 节点
child: 指向子 Fiber 节点
sibling:指向兄弟 fiber 节点
React 调和流程?
调和指的是将虚拟 DOM 映射到真实 DOM 的过程
包括 render 阶段 和 commit 阶段
render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。
触发调和过程的方式
在React 中有以下几种操作会触发调和过程:
- ReactDom.render() 和 ReactNativeRenderer.render()
- setState()
- forceUpdate() 的调用
- componentWillMount 和componentWillReceiveProp 中直接修改了state(地址)
- hooks 中的useReducer 和 useState 返回的钩子函数
两大阶段 render 和 commit 都做了哪些事情?
render阶段
总结
生成新的
fiber
节点,通过diff
算法对比节点差异创建出用于更新操作的workinprogressFiber
树,给需要更新的fiber
打上相对应的effectTag
,并且生成用于更新的effectList
链表- 具体可以拆分为
beginWork
以及completeWork
两个阶段,通过深度优先遍历的形式来进行这两个阶段
- 具体可以拆分为
相比于
react15
的递归处理虚拟 dom 节点,Reconciler
通过链表的形式改成了循环处理。每处理完一个fiber
节点都会检查时间是否充足或者是否有高优先级任务
具体做了什么
workLoop
- 整个 fiber 的遍历开始
- 执行每一个单元(fiber)的调度器
- 如果渲染没有被中断,那么 workLoop 会遍历一遍 fiber 树
performUnitOfWork
包括两个阶段 beginWork 和 completeUnitOfWork
beginWork
是向下调和的过程
就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点,打不同effectTag
completeUnitOfWork
- 是向上归并的过程
- 如果有兄弟节点,会返回 sibling 兄弟,没有返回 return 父级,一直返回到 fiebrRoot
- 期间可以形成effectList
- completeWork:对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理style,className等
这么一上一下,构成了整个 fiber 树的调和
扩展阅读
render 阶段当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。
因此 React 16 需要废弃以下生命周期:
- componentWillMount
- componentWillUpdate
- componentWillReceiveProps
这些生命周期都处于 render 阶段,因此都可能重复被执行,但是这些生命周期中可能会有setState()、fetch 发起异步请求、操作真实 DOM 等操作,而这些操作可能会伴随着生命周期的重启而被重复执行,可能会造成严重后果(例如一个付款的操作被重复执行),因此需要废弃。
commit阶段
总结
当前阶段不会被打断,会根据上面两阶段生成的
effectList
一口气执行完成渲染操作遍历
render
阶段生成的effectList
,effectList
上的Fiber
节点保存着对应的props
变化。之后会遍历effectList
进行对应的 dom 操作和生命周期、hooks 回调或销毁函数通过双缓存的技术
workInProgress Fiber
完成渲染后会变为current Fiber
树
具体做了什么
主要做的事就是执行effectList,更新DOM,执行生命周期,获取ref等操作
Before mutation(执行 DOM 操作前)
- 执行 getSnapshotBeforeUpdate
- 异步调用 useEffect
Mutation(执行 DOM 操作)
- 置空 ref
- 进行真实的 DOM 操作
Layout(执行 DOM 操作后)
- 对于类组件,会执行生命周期,setState 的callback
- 对于函数组件会执行 useLayoutEffect 钩子
- 如果有 ref ,会重新赋值 ref
什么是双缓冲树? 有什么作用?
双缓冲树:在内存中构建并直接替换的技术
canvas 绘制动画的时候,如果上一帧计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。为了解决这个问题,canvas 在内存中绘制当前动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况
React 用 workInProgress 树(内存中构建的树) 和 current (渲染树) 来实现更新逻辑
双缓存一个在内存中构建,一个渲染视图,两颗树用 alternate 指针相互指向,在下一次渲染的时候,直接复用缓存树做为下一次渲染树,上一次的渲染树又作为缓存树
这样可以防止只用一颗树更新状态的丢失的情况,又加快了 DOM 节点的替换与更新
对于状态丢失的补充:两颗树一个是用于渲染,另外一个在缓存中存在,如果用一颗fiber树,在更新中,如果存在更新失败,更新中断等情况,那么这种状态是不可逆的,而这种双缓冲树正好可以快速更新,又实在的保存上一次渲染的状态。
概念介绍
workInProgress
正在内存中构建的 Fiber 树
反映将要变化的UI
在一次更新中,所有的更新都是发生在 workInProgress 树上。在一次更新之后,workInProgress 树上的状态是最新的状态,那么它将变成 current 树用于渲染视图
current
- 反映当前屏幕的UI
alternate
- 内存中和渲染中的fiber两者用这个属性相互指向
Fiber 深度遍历流程?
1 | function App() { |
可以看到,遍历的过程中会从应用的根节点 rootFiber 开始,依次执行 beginWork(向下调和) 和completeWork(向上归并),最后形成一颗Fiber树,每个节点以 child 和 return 相连。
注意:当遍历到只有一个子节点的 Fiber 时,该 Fiber 节点的子节点不会执行 beginWork 和 completeWork,如图中的 ‘chen’ 文本节点。这是 react 的一种优化手段
Fiber的调和能中断吗? 如何中断?
render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的
Fiber 有优先级的概念
首先,每个更新任务都会被赋予一个优先级。当更新任务抵达调度器时,高优先级的更新任务(记为 A)会更快地被调度进 Reconciler 层;此时若有新的更新任务(记为 B)抵达调度器,调度器会检查它的优先级,若发现 B 的优先级高于当前任务 A,那么当前处于 Reconciler 层的 A 任务就会被中断,调度器会将 B 任务推入 Reconciler 层。当 B 任务完成渲染后,新一轮的调度开始,之前被中断的A 任务将会被重新推入 Reconciler 层,重新执行,这便是所谓“可恢复”。