跳到主要内容

认识

2023年07月17日
柏拉文
越努力,越幸运

一、认识


React FiberReact 内部定义的一种数据结构, 是 Fiber 树结构的节点单位, 也就是 React 16 新架构下的虚拟 DOMFiber 包含了元素信息、更新队列、类型、标记等。

React Fiber 解决了在 React 16 之前, 渲染任务都是同步的, 不可中断。这会在进行大量节点的reconcile时可能产生卡顿,因为浏览器所有的时间都交给了JS执行,并且JS的执行是单线程。React16之后就有了scheduler进行时间片的调度,给每个任务(工作单元)一定的时间,如果在这个时间内没有执行完,也要交出执行权给浏览器进行绘制和重排。所以异步可中断的更新需要一定的数据结构在内存中保存工作单元的信息。通过 Fiber 架构, React 得以实现了将不可中断的同步更新改为可中断的异步更新

二、工作


  1. 作为静态数据结构: 每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。

  2. 作为动态工作单元: 每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作。 updateQueue 存储了更新队列, flags 存储了要执行的操作

三、结构


Fiber对象上面保存了包括这个节点的属性、类型、dom等,Fiber通过childsiblingreturn(指向父节点)来形成Fiber树,还保存了更新状态时用于计算stateupdateQueueupdateQueue是一种链表结构,上面可能存在多个未计算的updateupdate也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。

export class FiberNode {
constructor(tag, pendingProps, key) {
// 存储节点的类型、实例、状态
this.tag = tag;
this.key = key;
this.type = null;
this.stateNode = null;

// 存储节点的父节点、子节点、兄弟节点信息
this.return = null;
this.sibling = null;
this.child = null;
this.index = 0;

// 存储节点的 ref 信息
this.ref = null;
this.refCleanup = null;

// 存储节点的 props、state、更新队列、依赖项信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.memoizedState = null;
this.updateQueue = null;
this.dependencies = null;

// 存储节点的双缓存信息
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;

// 存储节点的优先级、过期时间、双缓存信息
this.lanes = null;
this.childLanes = null;

// 存储节点的双缓存信息
this.alternate = null;
}
}

3.1 作为架构来说

作为架构来说,之前React15Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为Stack ReconcilerReact16Reconciler基于Fiber节点实现,被称为Fiber Reconciler

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 用于连接其他Fiber节点形成Fiber树
this.return = null; // 指向父级Fiber节点
this.child = null; // 指向子Fiber节点
this.sibling = null; // 指向右边第一个兄弟Fiber节点
this.index = 0;
}
注意

为什么父级指针叫做return而不是parent或者father呢? 答: 因为作为一个工作单元,return指节点执行完completeWork后会返回的下一个节点。子Fiber节点及其兄弟节点完成工作后会返回其父级节点,所以用return指代父级节点。

3.2 作为静态的数据结构来说

作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag; // Fiber对应组件的类型 Function/Class/Host...
this.key = key; // key属性
this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.type = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.stateNode = null; // Fiber对应的真实DOM节点
}

fiber.tagReact Element对应关系如下:

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 返回的组件

3.3 作为动态的工作单元来说

作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为动态的工作单元的属性


// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;

this.mode = mode;

// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;

this.firstEffect = null;
this.lastEffect = null;
}

3.4 Fiber 其他结构属性

此外,还有调度优先级相关指向该fiber在另一次更新时对应的fiber

function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {

// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;

// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}

四、双缓存


所谓双缓存是指在内存中构建并直接替换的技术。Fiber双缓存指的就是: 在经过Reconcile(diff)形成了新的workInProgress Fiber然后将workInProgress Fiber切换成current Fiber应用到真实dom中。存在双Fiber的好处是在内存中形成视图的描述,在最后应用到dom中,减少了对dom的操作。

4.1 Fiber两棵树

React中最多会从同时存在两颗Fiber树。当前屏幕上显示(呈现)的内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。

4.2 Fiber树连接

current Fiber树中的Fiber节点被称为current fiberworkInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

4.3 Fiber树切换

React应用的根节点通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber`树。

4.4 Fiber 双缓存过程

  • mount 阶段

    1. 首次执行React.render()或者ReactDOM.unstable_createRoot时,创建fiberRootrootFiber

      • fiberRoot: 整个应用的根节点,整个应用的根节点只有一个

      • rootFiber: 所在组件树根节点, 多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber

    2. fiberRootcurrent会指向当前页面上已渲染内容对应Fiber树,即current Fiber, 由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current指向的rootFiber没有任何子Fiber节点(即current Fiber树为空)。

      export function createFiberRoot(
      containerInfo: any,
      tag: RootTag,
      hydrate: boolean,
      hydrationCallbacks: null | SuspenseHydrationCallbacks,
      ): FiberRoot {
      const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
      if (enableSuspenseCallback) {
      root.hydrationCallbacks = hydrationCallbacks;
      }

      // Cyclic construction. This cheats the type system right now because
      // stateNode is any.
      const uninitializedFiber = createHostRootFiber(tag);
      root.current = uninitializedFiber;
      uninitializedFiber.stateNode = root;

      initializeUpdateQueue(uninitializedFiber);

      return root;
      }
    3. 进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。如果没有 alternate (初始化的 rootFiber 是没有 alternate ),那么会创建一个 fiber 作为 workInProgress 。会用 alternate 将新创建的 workInProgresscurrent 树建立起关联。这个关联过程只有初始化第一次创建 alternate 时候进行

      currentFiber.alternate = workInProgressFiber
      workInProgressFiber.alternate = currentFiber
    4. 已构建完的workInProgress Fiber树在commit阶段渲染到页面。fiberRootcurrent指针指向workInProgress Fiber树使其变为current Fiber

  • update 阶段

    1. 开启一次新的render阶段并构建一棵新的workInProgress Fiber,和mount时一样,workInProgress fiber的创建可以复用current Fiber树对应的节点数据

    2. workInProgress Fiber 树在 render 阶段完成构建后进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。

五、问题


5.1 为什么 React 有 Fiber 架构,但是 Vue 不需要 Fiber 架构

React因为先天的不足——无法精确更新,所以需要 React Fiber 把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;

5.2 之前递归遍历虚拟 Dom 树被打断就得从头开始,为什么有了 React Fiber 就能断点恢复呢?

React Fiber这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;

5.3 Fiber 解决了什么问题? 为什么 Fiber 能够解决卡顿的问题?

React 15 以及之前的版本,React 对于虚拟 DOM 是采用递归方式遍历更新的,比如一次更新,就会从应用根部递归更新,递归一旦开始,中途无法中断,随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,给前端交互上的体验就是卡顿。

React 16 为了解决卡顿问题引入了 fiber ,为什么它能解决卡顿,更新 fiber 的过程叫做 Reconciler(调和器),每一个 fiber 都可以作为一个执行单元来处理,所以每一个 fiber 可以根据自身的过期时间expirationTime( v17 版本叫做优先级 lane )来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘 repaints 之类的事情,这样就能给用户感觉不是很卡。然后等浏览器空余时间,在通过 scheduler (调度器),再次恢复执行单元上来,这样就能本质上中断了渲染,提高了用户体验。

5.4 说说对 Fiber 架构的理解?解决了什么问题?

参考资料


卡颂-React技术揭秘

全栈潇晨