从0到1认识React Fiber

Fiber为何被提出?

在React的组件更新时,对于计算机而言,它往往是一种CPU密集的操作,因为它要通过对比新旧虚拟DOM树,从而找出需要更新的内容,并通过打补丁的方式去将更新映射到真实的DOM树上。当页面的构成比较简单时,这个过程通常来说不会有太大的负担。但是当要对比的组件树非常多时,就会发生大量的新旧节点对比,CPU的压力也会变得愈加庞大,从而拉长了整个更新的时间,当时间超过16.6ms也就是一帧的时间时,用户往往就会感觉到明显的卡顿感。

在React 16之前,页面的整体更新操作,都是同步且不可中断的。换言之,当用户触发了某个页面更新的操作,到页面更新完成之前,整个页面都是完全被JS线程所阻塞的,做不了其他的任何事情。因为一旦中断,调用栈就会被销毁,中间的状态就丢失了。这种基于调用栈的实现,我们称为 Stack Reconcilation。

而在React 16之后,React采用了全新的Fiber架构作为组件的最小执行单元,并在架构层上新增了Scheduler(调度器)。通俗的来说,React将更新组件时大量的CPU计算的工作,利用"时间分片"的方案,将原本要一次性做的工作,拆分成一个个异步任务,在浏览器空闲的时间时执行。这种新的架构称为 Fiber Reconcilation。

在 React 中,Fiber 模拟之前的递归调用,具体通过链表的方式去模拟函数的调用栈,并且保存了父节点、兄弟节点、子节点的节点信息,这样就可以做到中断调用,将一个大的更新任务,拆分成小的任务,并设置优先级,在浏览器空闲的时异步执行。

这样一来,当浏览器存在优先级更高的任务,或者当前的更新操作耗时过长时,React可以主动的将线程权交还给浏览器去做优先级更高的任务或者绘制工作,等到浏览器空闲时,再回过头来完成被中断的更新操作。

Fiber是什么?

那么,Fiber是什么呢?

在书写React代码时,我们通常会使用JSX去描述页面的每一个节点。而等到代码执行时,React runtime会将我们书写的JSX代码,转化为Render Function,执行Render Function之后,会得到一个个对应的React Element,最终再通过React Element转化为最终的Fiber树结构。

可以说,Fiber在React里是一个个最小的工作单元,同时也保存着我们页面上每一个节点的对应信息。

js 复制代码
function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // 组件类型,比如 Function/Class/Host
  this.key = key; // key 唯一值,通常会在列表中使用
  this.elementType = null;
  this.type = null; // 元素类型,字符串或类或函数,比如 "div"/ComponentFn/Class
  this.stateNode = null; // 指向真实 DOM 对象
  // Fiber
  this.return = null; // 父 Fiber
  this.child = null; // 子 Fiber 的第一个
  this.sibling = null; // 下一个兄弟节点
  this.index = 0; // 在同级兄弟节点中的位置
  this.ref = null;
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;
  this.mode = mode;
  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;
  this.lanes = NoLanes;
  this.childLanes = NoLanes;
  this.alternate = null; 
  // ...
}

为了保证任务的可中断和可恢复性,Fiber会保存着每个节点对应的父节点、兄弟节点和子节点的信息, 例如:

javascript 复制代码
function App() {
  return (
    <div className="app">
      <span>hello</span>, Fiber
    </div>
  );
}

则对应的Fiber树为:

Fiber树是如何被构建的?

在React的整个渲染过程中,总体可以分为两个阶段:

  • render
  • commit

render阶段开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新,所谓的异步更新实则就是会根据浏览器是否存在空闲时间,来动态调整是否打断当前这一批次的更新工作,也就是concurrent模式。在React18中会默认开启。

scss 复制代码
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

而代码里的performUnitOfWork方法则是创建Fiber节点的起点,他里面所做的主要工作可以分为两部分:

  • beginwork
  • completework

具体两部分工作做了什么呢?

在beginwork的过程中,React会传入当前的Fiber节点,通过条件判断当前Fiber节点是否能够复用,如果能够复用则直接使用,如果不能则利用diff算法,创建出新的Fiber节点。

php 复制代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...
}
  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点

  • workInProgress:当前组件对应的Fiber节点

  • renderLanes:优先级

  • update时:如果current存在,在满足一定条件时可以复用current节点,这样就能克隆current.child作为workInProgress.child,而不需要新建workInProgress.child

  • mount时:除fiberRootNode以外,current === null。会根据fiber.tag不同,创建不同类型的子Fiber节点

php 复制代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {

  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    // ...省略

    // 复用current
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderLanes,
    );
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case IndeterminateComponent: 
      // ...省略
    case LazyComponent: 
      // ...省略
    case FunctionComponent: 
      // ...省略
    case ClassComponent: 
      // ...省略
    case HostRoot:
      // ...省略
    case HostComponent:
      // ...省略
    case HostText:
      // ...省略
    // ...省略其他类型
  }
}

类似beginWorkcompleteWork也是针对不同fiber.tag调用不同的处理逻辑。

php 复制代码
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

mount时的主要逻辑包括三个:

  • Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点
  • update逻辑中的updateHostComponent类似的处理props的过程
scss 复制代码
const currentHostContext = getHostContext();
// 为fiber创建对应DOM节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM节点赋值给fiber.stateNode
workInProgress.stateNode = instance;

// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}
相关推荐
全宝8 分钟前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇33 分钟前
前端模拟一个setTimeout
前端
萌萌哒草头将军37 分钟前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加1 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam2 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖3 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby3 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js
itslife3 小时前
Fiber 架构
前端·react.js
3Katrina3 小时前
妈妈再也不用担心我的课设了---Vibe Coding帮你实现期末课设!
前端·后端·设计
hubber3 小时前
一次 SPA 架构下的性能优化实践
前端