React Fiber 是什么?

2022-12-21 20:00:25 浏览数 (2)

大家好,我是前端西瓜哥。

为了提高 React 的性能,React 团队在开发 React 16 时做了底层的重构,引入了 React Fiber 的概念。

React Fiber 是什么?

Fiber,本意为 “纤维”,在计算机世界中则是 ”纤程“ 的意思。纤程可以看作是协程的一种,是一种 任务调度 方式。

JavaScript 是单线程的,有一个 event loop 的概念,它有一个有优先级的任务队列,只能按顺序执行一个任务,是不支持多个任务同时执行的。

这种设计的好处就是不用考虑多线程导致的顺序问题,并为此做一些加锁的额外逻辑,确保执行顺序符合预期。但也因为无法使用并行能力,在 CPU 密集的场景会有性能问题, 比如一个任务耗时过长会导致其他的任务,导致用户的交互响应发生延迟。

React 的组件更新是 CPU 密集的操作,因为它要做对比新旧虚拟 DOM 树的操作(diff,React 中 Reconcilation 负责),找出需要更新的内容(patch),通过打补丁的方式更新真实 DOM 树(React 中 Renderer 负责)。当要对比的组件树非常多时,就会发生大量的新旧节点对比,CPU 花费时间庞大,当耗时大大超过 16.6ms(一秒 60 帧的基准) 时,用户会感觉到明显的卡顿。

这一系列操作是通过递归的方式实现的,是 同步且不可中断 的。因为一旦中断,调用栈就会被销毁,中间的状态就丢失了。这种基于调用栈的实现,我们称为 Stack Reconcilation。

React 16 的一个重点工作就是优化更新组件时大量的 CPU 计算,最后选择了使用 “时间分片” 的方案,就是将原本要一次性做的工作,拆分成一个个异步任务,在浏览器空闲的时间时执行。这种新的架构称为 Fiber Reconcilation。

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

FiberNode

前面我们说到使用了链表的遍历来模拟递归栈调用,其中链表的节点 React 用 FiberNode 表示。

FiberNode 其实就是虚拟 DOM,它记录了:

  1. 节点相关类型,比如 tag 表示组件类型、type 表示元素类型等;
  2. 节点的指向;
  3. 副作用相关的属性;
  4. lanes 是关于调度优先级的;
代码语言:javascript复制
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 通过 return 指向父 Fiber,child 指向子 Fiber 的首位、sibling 指向下一个兄弟节点。通过它们我们其实就能拿到一个完整的结构树。

对于:

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

形成的 Fiber 树为:

其中弧线为调用顺序。紫色为 beginWork、粉色为 completeWork。beginWork 是 “递” 的过程,而 comleteWork 则是 “归” 的过程。

为什么不用 generator 或 async/await?

generator 和 async/await 也可以做到在函数中间暂停函数执行的逻辑,将执行让出去,能做到将同步变成异步。

但 React 没有选择它们,这是因为:

  1. 具有传染性,比如一个函数用了 async,调用它的函数就要加上 async,有语法开销,此外也会有性能上的额外开销;
  2. 无法在 generator 和 async/await 中恢复一些中间状态。

具体见官方的 github issue 讨论:

https://github.com/facebook/react/issues/7942#issuecomment-254987818

Scheduler

做了时间分片,拆分了多个任务,React 就可以以此为基石,给任务设置优先级。

React 实现了一个 Scheduler(调度器)来实现任务调度执行,并单独抽离为一个单独的包,它会在浏览器有空闲的时候执行。其实浏览器也提供了一个 requestIdleCallback 的 API,支持这个能力,但兼容性实在不好,React 还是自己实现了一套。

这个 Scheduler 支持优先级,底层使用了 小顶堆,确保能高效拿到最快要过期的任务,然后执行它。

小顶堆,其实就是优先级队列。小顶堆在结构上是一个完全二叉树,但能保证每次从堆顶取出元素时,是最小的元素。

任务的 优先级 分为几种:

  1. NoPriority:无优先级
  2. ImmediatePriority:立即执行
  3. UserBlockingPriority:用户阻塞优先级,不执行可能会导致用户交互阻塞
  4. NormalPriority:普通优先级
  5. LowPriority:低优先级
  6. IdlePriority:空闲优先级

React 自身也有优先级,叫做 Lane,两者是不同的。

结尾

React 的架构过于宏大,今天先随便说一点吧。

总的来说,React Fiber 是在 React 16 中引入的新的架构,将原本同步不可中断的更新,变成异步可中断更新,将原本一个耗时的大任务做了时间分片,拆分成一个个小任务,在浏览器空闲的时间执行。此外添加优先级的概念,将一些重要的任务先执行,比如一些用户交互的响应函数。

一切为了更好的用户体验。

我是前端西瓜哥,欢迎关注我,学习更多前端知识。

0 人点赞