zl程序教程

您现在的位置是:首页 >  Javascript

当前栏目

一篇带你了解 React Fiber 是什么?

2023-02-19 12:23:49 时间

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

为了提高 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 是关于调度优先级的。
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 指向下一个兄弟节点。通过它们我们其实就能拿到一个完整的结构树。

对于:

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

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