Fiber 与 调和

什么是 Fiber ?

React 新一代调和(Reconcilation)引擎,同时也是一种数据结构

React 的 虚拟 dom,最小粒度的执行单元,可以设置不同的优先级

Fiber 保存了哪些信息?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function FiberNode(){
this.tag = tag; // fiber 标签 证明是什么类型fiber。
this.key = key; // key调和子节点时候用到。
this.type = null; // dom元素是对应的元素类型,比如div,组件指向组件对应的类或者函数。
this.stateNode = null; // 指向对应的真实dom元素,类组件指向组件实例,可以被ref获取。

this.return = null; // 指向父级fiber
this.child = null; // 指向子级fiber
this.sibling = null; // 指向兄弟fiber
this.index = 0; // 索引

this.ref = null; // ref指向,ref函数,或者ref对象。

this.pendingProps = pendingProps;// 在一次更新中,代表element创建
this.memoizedProps = null; // 记录上一次更新完毕后的props
this.updateQueue = null; // 类组件存放setState更新队列,函数组件存放
this.memoizedState = null; // 类组件保存state信息,函数组件保存hooks信息,dom元素为null
this.dependencies = null; // context或是时间的依赖项

this.mode = mode; //描述fiber树的模式,比如 ConcurrentMode 模式

this.effectTag = NoEffect; // effect标签,用于收集effectList
this.nextEffect = null; // 指向下一个effect

this.firstEffect = null; // 第一个effect
this.lastEffect = null; // 最后一个effect

this.expirationTime = NoWork; // 通过不同过期时间,判断任务是否过期, 在v17版本用lane表示。

this.alternate = null; //双缓存树,指向缓存的fiber。更新阶段,两颗树互相交替。
}

element、fiber、dom三种什么关系?

element

  • React 视图层在代码层级上的表象

  • 开发者写的 jsx 语法,写的元素结构,都会被创建成 element 对象的形式

  • 上面保存了 props , children 等信息

dom

  • 元素在浏览器上给用户直观的表象

fiber

  • element 和真实 DOM 之间的交流枢纽站

  • 一方面每一个类型 element 都会有一个与之对应的 fiber 类型,element 变化引起更新流程都是通过 fiber 层面做一次调和改变,然后对于元素,形成新的 DOM 做视图渲染

    img

element 与 fiber 之间的对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const FunctionComponent = 0;       // 对应函数组件
export const ClassComponent = 1; // 对应的类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为跟元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件

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 节点

image-20220330164501964

React 调和流程?

调和指的是将虚拟 DOM 映射到真实 DOM 的过程

包括 render 阶段 和 commit 阶段

render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。

触发调和过程的方式

在React 中有以下几种操作会触发调和过程:

  • ReactDom.render() 和 ReactNativeRenderer.render()
  • setState()
  • forceUpdate() 的调用
  • componentWillMount 和componentWillReceiveProp 中直接修改了state(地址)
  • hooks 中的useReducer 和 useState 返回的钩子函数

两大阶段 render 和 commit 都做了哪些事情?

render阶段

总结

  1. 生成新的 fiber 节点,通过 diff 算法对比节点差异创建出用于更新操作的 workinprogressFiber 树,给需要更新的 fiber 打上相对应的 effectTag,并且生成用于更新的 effectList 链表

    • 具体可以拆分为 beginWork 以及 completeWork 两个阶段,通过深度优先遍历的形式来进行这两个阶段
  2. 相比于 react15 的递归处理虚拟 dom 节点,Reconciler 通过链表的形式改成了循环处理。每处理完一个 fiber 节点都会检查时间是否充足或者是否有高优先级任务

具体做了什么

image-20220331165439170

workLoop

  • 整个 fiber 的遍历开始
  • 执行每一个单元(fiber)的调度器
  • 如果渲染没有被中断,那么 workLoop 会遍历一遍 fiber 树

performUnitOfWork

包括两个阶段 beginWork 和 completeUnitOfWork

beginWork

  • 是向下调和的过程

  • 就是由 fiberRoot 按照 child 指针逐层向下调和,期间会执行函数组件,实例类组件,diff 调和子节点,打不同effectTagimage-20220331165933010

completeUnitOfWork

  • 是向上归并的过程
  • 如果有兄弟节点,会返回 sibling 兄弟,没有返回 return 父级,一直返回到 fiebrRoot
  • 期间可以形成effectList
  • completeWork:对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理style,className等

这么一上一下,构成了整个 fiber 树的调和

扩展阅读

render 阶段当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。

因此 React 16 需要废弃以下生命周期:

  • componentWillMount
  • componentWillUpdate
  • componentWillReceiveProps

这些生命周期都处于 render 阶段,因此都可能重复被执行,但是这些生命周期中可能会有setState()、fetch 发起异步请求、操作真实 DOM 等操作,而这些操作可能会伴随着生命周期的重启而被重复执行,可能会造成严重后果(例如一个付款的操作被重复执行),因此需要废弃。

commit阶段

总结

  1. 当前阶段不会被打断,会根据上面两阶段生成的 effectList 一口气执行完成渲染操作

  2. 遍历 render 阶段生成的 effectListeffectList 上的 Fiber 节点保存着对应的 props 变化。之后会遍历 effectList 进行对应的 dom 操作和生命周期、hooks 回调或销毁函数

  3. 通过双缓存的技术 workInProgress Fiber 完成渲染后会变为 current Fiber

具体做了什么

主要做的事就是执行effectList,更新DOM,执行生命周期,获取ref等操作

image-20220331174523687

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
2
3
4
5
6
7
8
9
10
function App() {
return (
<div>
xiao
<p>chen</p>
</div>
)
}

ReactDOM.render(<App />, document.getElementById("root"));

img

可以看到,遍历的过程中会从应用的根节点 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 层,重新执行,这便是所谓“可恢复”