跳到主要内容

认识

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

一、认识


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

Fiber 作为静态数据结构, 每个 Fiber 节点对应一个 ReactElement,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。Fiber 作为动态工作单元, 每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue 存储了更新队列, flags 存储了要执行的操作, lang 表示当前更新优先级, childLanes 表示当前 Fiber 需要更新。 Fiber 通过childsiblingreturn(指向父节点)来形成Fiber树。为什么父级指针叫做return而不是parent或者father呢?, 因为作为一个工作单元, return指节点执行完 completeWork 后会返回的下一个节点, 子Fiber节点及其兄弟节点完成工作后会返回其父级节点, 所以用return指代父级节点。

Fiber 工作: JSX 会编译为 ReactElement, 在 Render BeginWork 递阶段, 从根节点 Fiber 开始, 从上而下根据 ReactElement 生成新的 Fiber 节点。如果有旧 Fiber, 会基于 Reconcile Diff 比较 ReactElement 和 旧 Fiber, 对比差异是否生成新的 Fiber。此时, 内存中有两颗 Fib er 树, Fiber 双缓冲树 在经过 Reconcile(diff)形成了新的 workInProgress Fiber 然后将 workInProgress Fiber 切换成 Current Fiber 应用到 真实 DOM 中。 存在 Fiber 的好处是在内存中形成视图的描述,在最后应用到 DOM 中,减少了对dom的操作。

二、结构


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;
}
}

2.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指代父级节点。

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

2.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;
}

2.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的操作。

3.1 Fiber两棵树

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

3.2 Fiber树连接

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

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

3.3 Fiber树切换

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

3.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 树。

四、问题


4.1 React Fiber 是如何实现更新过程可控?

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

React 16 之后: 为了解决卡顿问题引入了 Fiber, Fiber 作为动态的工作单元来说, 每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue 存储了更新队列, flags 存储了要执行的操作, lang 表示当前更新优先级, childLanes 表示当前 Fiber 需要更新。而且, Fiber 节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber 架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler 调度器使用 MessageChannelsetTimeout 控制每个任务的执行时长, Scheduler 能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。

Scheduler 调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTimecurrentTime , 如果 startTime > currentTime, 表示当前任务未就绪, 存入 timerQueue。并根据开始时间重新排列 timerQueue 中任务的顺序。当 timerQueue 中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueuetimerQueuetaskQueue 两个队列为了能在 O(1) 的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务Scheduler 调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现

React Fiber 在更新过程中, 整体渲染被拆分为两个阶段, Render 阶段用于生成一棵新的 Fiber 树,并为后续的 DOM 更新准备好 副作用列表(effect list, Render 阶段的任务可随时中断, 交给 Scheduler 控制, 这样能确保 React 应用流畅响应用户交互; Commit 阶段是 React 更新流程中唯一会直接操作 DOM 的阶段,它根据 Render 阶段生成的副作用列表执行实际的更新操作。Commit 阶段同步执行, 批量更新 DOM, 保证用户体验一致性和页面稳定。简单来说,由于 Render 阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,所以这个过程必须用同步渲染来求稳。

4.2 Fiber 为什么是 React 性能的一个飞跃?

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

React 16 之后: 为了解决卡顿问题引入了 Fiber, Fiber 作为动态的工作单元来说, 每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue 存储了更新队列, flags 存储了要执行的操作, lang 表示当前更新优先级, childLanes 表示当前 Fiber 需要更新。而且, Fiber 节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber 架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler 调度器使用 MessageChannelsetTimeout 控制每个任务的执行时长, Scheduler 能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。

Scheduler 调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTimecurrentTime , 如果 startTime > currentTime, 表示当前任务未就绪, 存入 timerQueue。并根据开始时间重新排列 timerQueue 中任务的顺序。当 timerQueue 中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueuetimerQueuetaskQueue 两个队列为了能在 O(1) 的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务Scheduler 调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现

React Fiber 在更新过程中, 整体渲染被拆分为两个阶段, Render 阶段用于生成一棵新的 Fiber 树,并为后续的 DOM 更新准备好 副作用列表(effect list, Render 阶段的任务可随时中断, 交给 Scheduler 控制, 这样能确保 React 应用流畅响应用户交互; Commit 阶段是 React 更新流程中唯一会直接操作 DOM 的阶段,它根据 Render 阶段生成的副作用列表执行实际的更新操作。Commit 阶段同步执行, 批量更新 DOM, 保证用户体验一致性和页面稳定。简单来说,由于 Render 阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,所以这个过程必须用同步渲染来求稳。

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

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

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

React 16 之后: 为了解决卡顿问题引入了 Fiber, Fiber 作为动态的工作单元来说, 每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue 存储了更新队列, flags 存储了要执行的操作, lang 表示当前更新优先级, childLanes 表示当前 Fiber 需要更新。而且, Fiber 节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber 架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler 调度器使用 MessageChannelsetTimeout 控制每个任务的执行时长, Scheduler 能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。

Scheduler 调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTimecurrentTime , 如果 startTime > currentTime, 表示当前任务未就绪, 存入 timerQueue。并根据开始时间重新排列 timerQueue 中任务的顺序。当 timerQueue 中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueuetimerQueuetaskQueue 两个队列为了能在 O(1) 的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务Scheduler 调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现

因此, 由于 React 历史渲染的设计思想, 通过引入 Fiber 架构, 来支持异步、可中断的任务调度, 保证关键任务优先执行, 使得 UI 更新更平滑。

Vue 从设计之初开始, 就采用响应式依赖追踪机制, 基于 Object.defineProperty 或者 Proxy 来监听数据变化, 当数据变化时, Vue 能精确地知道哪些组件受到了影响, 从而只对必要的组件进行更新, 这种精细的依赖收集机制使得更新过程本身就较为高效。Vue 在更新过程中会将多次数据变更批量收集,然后在下一个事件循环中统一执行更新(通常借助 nextTick 实现异步刷新)。这种方式能有效避免频繁的重复渲染,也确保了界面的稳定性。

因此, 由于 Vue 的响应式系统天然能够精确定位需要更新的部分,加上批量更新策略,在大多数常规应用场景下,其同步更新的模式已能满足性能需求,无需像 React 那样引入复杂的时间切片和任务调度机制。

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

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

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

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

React 16 之后: 为了解决卡顿问题引入了 Fiber, Fiber 作为动态的工作单元来说, 每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue 存储了更新队列, flags 存储了要执行的操作, lang 表示当前更新优先级, childLanes 表示当前 Fiber 需要更新。而且, Fiber 节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber 架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler 调度器使用 MessageChannelsetTimeout 控制每个任务的执行时长, Scheduler 能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。

Scheduler 调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTimecurrentTime , 如果 startTime > currentTime, 表示当前任务未就绪, 存入 timerQueue。并根据开始时间重新排列 timerQueue 中任务的顺序。当 timerQueue 中有任务就绪,即startTime <= currentTime,我们将其取出并加入taskQueuetimerQueuetaskQueue 两个队列为了能在 O(1) 的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务Scheduler 调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现

React Fiber 在更新过程中, 整体渲染被拆分为两个阶段, Render 阶段用于生成一棵新的 Fiber 树,并为后续的 DOM 更新准备好 副作用列表(effect list, Render 阶段的任务可随时中断, 交给 Scheduler 控制, 这样能确保 React 应用流畅响应用户交互; Commit 阶段是 React 更新流程中唯一会直接操作 DOM 的阶段,它根据 Render 阶段生成的副作用列表执行实际的更新操作。Commit 阶段同步执行, 批量更新 DOM, 保证用户体验一致性和页面稳定。简单来说,由于 Render 阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit 阶段的操作则涉及真实 DOM 的渲染,所以这个过程必须用同步渲染来求稳。

总结来说, React 采用 Fiber 架构主要是为了支持异步、可中断的任务调度,从而提升在复杂场景下的用户体验

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

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

Fiber 作为静态数据结构, 每个 Fiber 节点对应一个 ReactElement,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。Fiber 作为动态工作单元, 每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue 存储了更新队列, flags 存储了要执行的操作, lang 表示当前更新优先级, childLanes 表示当前 Fiber 需要更新。 Fiber 通过childsiblingreturn(指向父节点)来形成Fiber树。为什么父级指针叫做return而不是parent或者father呢?, 因为作为一个工作单元, return指节点执行完 completeWork 后会返回的下一个节点, 子Fiber节点及其兄弟节点完成工作后会返回其父级节点, 所以用return指代父级节点。

Fiber 工作: JSX 会编译为 ReactElement, 在 Render BeginWork 递阶段, 从根节点 Fiber 开始, 从上而下根据 ReactElement 生成新的 Fiber 节点。如果有旧 Fiber, 会基于 Reconcile Diff 比较 ReactElement 和 旧 Fiber, 对比差异是否生成新的 Fiber。此时, 内存中有两颗 Fib er 树, Fiber 双缓冲树 在经过 Reconcile(diff)形成了新的 workInProgress Fiber 然后将 workInProgress Fiber 切换成 Current Fiber 应用到 真实 DOM 中。 存在 Fiber 的好处是在内存中形成视图的描述,在最后应用到 DOM 中,减少了对dom的操作。

总结来说, React 采用 Fiber 架构主要是为了支持异步、可中断的任务调度,从而提升在复杂场景下的用户体验

参考资料


卡颂-React技术揭秘

全栈潇晨