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 阶段生成的 effectList ,effectList 上的 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 层,重新执行,这便是所谓“可恢复”。