「从0实现React18系列」Fiber架构的实现原理

开发 架构
在本节我们对Reconciler的架构有了大概的认知,了解了传统的库与现代框架的工作原理,也掌握了预编译和即时编译的区别,以及它们在现代框架中的应用。

Reconciler 是什么

Reconciler 是 React 核心逻辑所在的模块,中文名叫协调器。

Reconciler 架构介绍

在React中,Reconciler​(协调器)是负责管理虚拟DOM树更新的关键部分。当组件状态或属性发生更改时,Reconciler的任务是确定如何有效地更新DOM来反映这些更改。这个过程通常被称为 "协调"(Reconciliation)。

Reconciler​的核心思想是通过将新的虚拟DOM树与旧的虚拟DOM树进行比较,找出需要实际更新的部分,然后最小化实际DOM操作的数量。这个过程被称为"diffing"算法。

传统库与现代框架的工作原理

在传统的库(jQuery)工作原理(过程驱动)。

图片

在传统的前端开发中使用的jQuery​库的工作原理主要是通过一个简化和统一的API,使得开发者能够更容易地操作DOM、处理事件、创建动画以及发起AJAX请求等。而不是描述UI的状态。所以jQuery的工作原理是过程驱动的。

现代的前端框架结构与工作原理(状态驱动)。

图片

现代的前端框架结构与工作原理

  • 在现代的前端框架中,开发者使用描述UI的方法(template或JSX)来定义组件和它们之间的关系。
  • 运行时核心模块根据描述的UI,管理组件的创建、更新和销毁,处理数据状态变更,并通过虚拟DOM或响应式系统来优化UI更新性能。
  • 当需要与宿主环境交互时(如操作DOM、处理事件或发起网络请求),运行时核心模块会调用宿主环境API。前端框架通常会封装这些API,提供统一的跨平台接口。

AOT预编译 与 JIT即时编译

现代框架都需要“编译”这一步骤,用于:

  • 将“框架中描述的UI”转换为宿主环境可识别的代码。
  • 代码转化,比如将ts编译成js,实现polyfill等。
  • 执行一些编译时优化。

“编译”可以选择两个时机执行:

  • 代码在构建时,被称为 AOT(Ahead Of Time,提前编译或预编译),宿主环境获得是编译后的代码
  • 代码在宿主环境执行时,被称为 JIT(Just In Time,即时编译),代码在宿主环境中编译并执行

大部分采用模板语法描述UI的前端框架都会进行AOT优化,例如:Vue3、Angular、Svelte。

其本质原因在于模板语法时固定的,固定意味着“可分析”,“可分析”意味着在编译时可以标记模板语法中的静态部分(不变的部分)与动态部分(包含自变量、可变的部分)。

但采用JSX语法描述UI的前端框架很难从 AOT中受益,因为JSX是ES的语法糖,ES语句的灵活性使其很难进行静态分析。

拓展 那么Template语法是如何从中受益的呢?

  1. 解析:将模板字符串解析成抽象语法树(AST)。AST是一种树形结构,用于表示模板中的元素、属性、文本节点等。
  2. 优化:遍历AST,对其中的静态内容(如纯文本节点、静态属性等)进行标记。这些标记在后续的渲染过程中有助于避免不必要的计算和更新,从而提高性能。
  3. 代码生成:将优化后的AST转换成可执行的JavaScript代码。这通常包括生成渲染函数(render function)和虚拟DOM节点。渲染函数用于创建和更新实际的DOM结构。

模板语法由于在构建时已经被编译成可执行的JavaScript代码,运行时无需再进行解析和编译,从而减少了性能开销

ReactElement 数据结构的不足

回归主题,根据前面的学习,我们知道了 JSX 方法执行后会返回一个新的 React 元素(ReactElement)。React 元素是一个轻量级的对象,描述了要渲染的 UI 组件的类型(type)、属性(props),和子元素(children)等信息。

这里可以给自己个问题,如果ReactElement​作为reconciler核心模块操作的数据结构,会存在哪些问题:

  • 无法表达ReactElement节点与另一个ReactElement节点之间的关系(因为它只记录了自身的数据,比如组件的类型、属性和子元素等),一般把ReactElement称为React的数据存储单元。
  • 字段有限,不好拓展(比如:无法表达状态)。

从下图中可以看到,ReactELement这种数据结构很有限,在节点属性关联方面也只有 children,并没有保存兄弟节点以及父节点之间的关系:

图片

当然在React 16版本之前,React 使用的是名为Stack Reconciler的旧调和算法。Stack Reconciler 的核心是递归遍历组件树,把数据保存在递归调用栈中。它使用的深层递归遍历方法。

但是使用递归遍历组件树时,会导致一些问题:

  • 阻塞主线程:在 JavaScript 中,递归调用可能会阻塞主线程,因为 JavaScript 是单线程的。如果组件树很大或者更新很频繁,递归调用可能会导致 UI 变得不流畅,影响用户体验。
  • 没有优先级调度:Stack Reconciler 无法对不同的更新任务进行优先级调度,所有的更新任务都会被视为相同的优先级。这意味着对于高优先级的任务(如动画或用户交互),React 无法优先处理,从而可能导致性能下降。

为了解决这些问题,React 引入了 Fiber Reconciler​。Fiber Reconciler​ 使用了一种名为 "Fiber" 的新数据结构来表示组件树。

它的特点:

  • 介于ReactElement与真实UI节点之间。
  • 能够表达节点之间的关系。
  • 方便拓展,不仅作为数据存储单元,也能作为工作单元

FiberNode 是虚拟DOM在React中的实现。

FiberNode Tree的数据结构如图所示:

图片

FiberNode 上有很多属性,包括和自身相关的属性 ref,节点之间的关系 return、silbing还有工作单元上的属性,比如 pendingProps等等,后面会详细介绍。

Fiber出现的意义

Fiber最主要的两层含义:

  1. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
  2. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。

Fiber的出现也为React带来了很多意义:

从优化层面来说,Fiber是一种新的调和算法(reconciliation algorithm)。

  1. 增量渲染:在早期的 React 版本(Stack Reconciler)中,当有组件更新时,React 会一次性完成整个组件树的调和过程。这会导致长时间的 JavaScript 执行阻塞,从而影响用户界面的响应性。Fiber 引入了增量渲染的概念,允许将调和过程分成多个小任务,这些任务可以在浏览器的空闲时间内执行。这样,即使在复杂的应用程序中,React 也能实现更平滑的用户界面更新。
  2. 任务调度:Fiber 引入了任务优先级的概念,使得 React 可以根据任务的优先级来调度它们的执行。这意味着较高优先级的任务(如用户交互事件)可以打断较低优先级的任务(如数据加载),从而实现更灵活的任务调度。这有助于提高应用程序的响应性和性能。

这两个概念会在后面的章节详细讲解。

Fiber是什么?

Fiber是React的最小的工作单元。在React的世界中,一切都可以是组件。在普通的HTML页面上,开发者们可以将多个DOM元素整合在一起组成一个组件。

普通的DOM元素(HostComponent)可以是组件,普通的文本节点(HostText)也可以是组件。还有通过ReactDom.render方法创建的根元素(RootElement)也可以是组件,还有经常在React中使用的函数组件(FunctionComponent)。

在React源码中,每个FiberNode都有一个WorkTag属性,用于标识当前节点的类型。

// pagkages/react-reconciler/src/ReactWorkTags.ts
export type WorkTag =
| typeof FunctionComponent
| typeof ClassComponent
| typeof HostRoot
| typeof HostComponent
| typeof HostText
| typeof Fragment;

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const HostRoot = 3; // 通过ReactDom.render()产生的根元素

export const HostComponent = 5; // dom元素 比如 <div></div>
export const HostText = 6; // 文本类型 比如:<div>123</div>
export const Fragment = 7; // <Fragment />

ReactWorkTags.ts文件中定义了所有可能的节点类型,每个类型都应一个number类型的值。

这样做的好处是可以通过比较两个节点的 WorkTag 属性来判断它们是否是同一类型的节点,而不需要通过字符串比较等方式,这样可以提高比较的效率,也可以减少出错的可能性。

每一个组件都对应着一个FiberNode​,许多个FiberNode​互相嵌套、关联就组成了FiberNode Tree​。正如下面表示的FiberNode Tree和DOM树的关系一样:

Fiber树                    DOM树

div#root div#root
| |
<App/> div
| / \
div p a
/
/
p ----> <Child/>
|
a

一个DOM节点一定对应着一个FiberNode,但每一个Fiber节点缺不一定有对应的DOM节点。

因为React支持不同类型的组件,因此每个FiberNode并不一定具有对应的DOM节点。

  • 函数组件:函数组件是一个简单的函数,它接收属性(props)并返回JSX。这个JSX可能包含DOM节点,但这个DOM节点并不是真实的DOM节点,而是当React渲染组件时,它会将函数组件返回的JSX转换成真实的DOM节点。所以函数组件本身并不会直接映射到一个DOM节点。
  • Fragment:React Fragment是一种特殊的组件,用于在不添加额外DOM节点的情况下返回多个子元素。当遍历组件树时,React会将Fragment的子元素视为直接子元素,而不会为Fragment本身创建DOM节点。
  • 还有很多,不一一举例了。

Fiber工作单元的结构

Fiber作为工作单元,它有很多属性:

  • Fiber实例属性: tag、key、type、stateNode等。
  • 与其它节点关系的链表属性:return、child、sibling、index。
  • Ref相关的属性:ref。
  • Fiber更新相关的属性:pendingProps、memoizedProps、memoizedState、updateQueue、alternate。
  • Fiber Effect:flags、subtreeFlags、deletions。
// pagkages/react-reconciler/src/ReactFiber.js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
) {
// Instance
this.tag = tag;
this.key = key;
this.type = null; // fiber对应的DOM元素的标签类型,div、p...
this.stateNode = null; // fiber的实例,类组件场景下,是组件的类,HostComponent场景,是dom元素

this.ref = null; // ref相关

// Fiber 除了有自身实例上的属性,还需要有表示和其它节点的关系
this.return = null; // 指向父级fiber
this.child = null; // 指向子fiber
this.sibling = null; // 同级兄弟fiber
this.index = 0;

// 作为工作单元与Fiber更新相关
this.pendingProps = pendingProps; // 刚开始工作阶段的 props
this.memoizedProps = null; // 工作结束时确定下来的 props
this.memoizedState = null; // 更新完成后的新 state
this.updateQueue = null; // Fiber产生的更新操作都会放在更新队列中

// Effects
this.flags = NoFlags; // 比如插入 更改 删除dom等)初始状态时表示没有任何标记(因为还没进行fiberNode对比)
this.subtreeFlags = NoFlags; // 子节点副作用标识
this.deletions = null; // 用于存放被删除的子节点

/*
* 可以看成是workInProgress(或current)树中的和它一样的节点,
* 可以通过这个字段是否为null判断当前这个fiber处在更新还是创建过程
* */
this.alternate = null; // 用于 current Fiber树和 workInProgress Fiber树的切换(如果当时fiberNode树是current树,则alternate指向的是workInProgress树)
}

这里Fiber节点的属性没有写完全,可以去react源码里看,地址在代码块首行。

虽然属性很多,但可以按三层含义将它们分类来看:

作为架构来说

每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

举个例子,比如下面的组件结构:

function App() {
return (
<div>
i am
<span>时光屋小豪</span>
<span>fighting</span>
</div>
)
}

对应的FiberNode Tree结构:

图片

作为静态的数据结构

作为静态的数据结构,需要保存组件的相关的信息:

// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null;

作为动态的工作单元

作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息,会在后续的更新流程章节中使用到具体属性时再详细介绍。

// 保存本次更新造成的状态改变相关信息
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;

如下两个字段保存调度优先级相关的信息,会在讲解Scheduler时介绍。

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

总结

在本节我们对Reconciler的架构有了大概的认知,了解了传统的库与现代框架的工作原理,也掌握了预编译和即时编译的区别,以及它们在现代框架中的应用。

在上一节中,我们实现了JSX的转换,知道了React Element这种数据,但是它也有一定的缺陷,为了解决这个缺陷,React 引入了Fiber架构,介绍了Fiber出现的意义,以及它的结构是什么样的,通过FiberNode组成的FiberNode Tree的结构。

下一节主要介绍Fiber作为Reconciler核心模块的工作单元,是如何创建及更新DOM的。

责任编辑:姜华 来源: 前端时光屋
相关推荐

2023-03-28 07:59:57

ReactReconciler

2024-04-24 11:00:05

React 18Fiber

2022-04-16 20:10:00

React Hookfiber框架

2021-06-16 06:05:25

React18React

2021-11-01 19:49:55

React组件模式

2023-12-01 09:14:58

ReactFiber

2021-06-22 07:45:57

React18startTransiReact

2022-03-16 17:01:35

React18并发的React组件render

2021-06-22 07:30:07

React18Automatic b自动批处理

2022-04-27 07:37:42

ReactReact18

2022-03-30 14:22:55

ReactReact18并发特性

2022-07-06 08:30:36

vuereactvdom

2022-07-13 15:23:57

Vue fiberreact前端

2020-10-28 09:12:48

React架构Hooks

2021-12-16 06:21:16

React组件前端

2021-11-29 06:05:31

React组件前端

2022-02-11 13:44:56

fiber架构React

2022-05-03 21:18:38

Vue.js组件KeepAlive

2022-07-03 20:53:23

React18请求数据

2021-06-15 14:54:23

ReactReact 18SSR
点赞
收藏

51CTO技术栈公众号