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语法是如何从中受益的呢?
- 解析:将模板字符串解析成抽象语法树(AST)。AST是一种树形结构,用于表示模板中的元素、属性、文本节点等。
- 优化:遍历AST,对其中的静态内容(如纯文本节点、静态属性等)进行标记。这些标记在后续的渲染过程中有助于避免不必要的计算和更新,从而提高性能。
- 代码生成:将优化后的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最主要的两层含义:
- 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
- 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)。
Fiber的出现也为React带来了很多意义:
从优化层面来说,Fiber是一种新的调和算法(reconciliation algorithm)。
- 增量渲染:在早期的 React 版本(Stack Reconciler)中,当有组件更新时,React 会一次性完成整个组件树的调和过程。这会导致长时间的 JavaScript 执行阻塞,从而影响用户界面的响应性。Fiber 引入了增量渲染的概念,允许将调和过程分成多个小任务,这些任务可以在浏览器的空闲时间内执行。这样,即使在复杂的应用程序中,React 也能实现更平滑的用户界面更新。
- 任务调度:Fiber 引入了任务优先级的概念,使得 React 可以根据任务的优先级来调度它们的执行。这意味着较高优先级的任务(如用户交互事件)可以打断较低优先级的任务(如数据加载),从而实现更灵活的任务调度。这有助于提高应用程序的响应性和性能。
这两个概念会在后面的章节详细讲解。
Fiber是什么?
Fiber是React的最小的工作单元。在React的世界中,一切都可以是组件。在普通的HTML页面上,开发者们可以将多个DOM元素整合在一起组成一个组件。
普通的DOM元素(HostComponent)可以是组件,普通的文本节点(HostText)也可以是组件。还有通过ReactDom.render方法创建的根元素(RootElement)也可以是组件,还有经常在React中使用的函数组件(FunctionComponent)。
在React源码中,每个FiberNode都有一个WorkTag属性,用于标识当前节点的类型。
ReactWorkTags.ts文件中定义了所有可能的节点类型,每个类型都应一个number类型的值。
这样做的好处是可以通过比较两个节点的 WorkTag 属性来判断它们是否是同一类型的节点,而不需要通过字符串比较等方式,这样可以提高比较的效率,也可以减少出错的可能性。
每一个组件都对应着一个FiberNode,许多个FiberNode互相嵌套、关联就组成了FiberNode Tree。正如下面表示的FiberNode Tree和DOM树的关系一样:
一个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。
这里Fiber节点的属性没有写完全,可以去react源码里看,地址在代码块首行。
虽然属性很多,但可以按三层含义将它们分类来看:
作为架构来说
每个Fiber节点有个对应的React element,多个Fiber节点是如何连接形成树呢?靠如下三个属性:
举个例子,比如下面的组件结构:
对应的FiberNode Tree结构:
作为静态的数据结构
作为静态的数据结构,需要保存组件的相关的信息:
作为动态的工作单元
作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息,会在后续的更新流程章节中使用到具体属性时再详细介绍。
如下两个字段保存调度优先级相关的信息,会在讲解Scheduler时介绍。
总结
在本节我们对Reconciler的架构有了大概的认知,了解了传统的库与现代框架的工作原理,也掌握了预编译和即时编译的区别,以及它们在现代框架中的应用。
在上一节中,我们实现了JSX的转换,知道了React Element这种数据,但是它也有一定的缺陷,为了解决这个缺陷,React 引入了Fiber架构,介绍了Fiber出现的意义,以及它的结构是什么样的,通过FiberNode组成的FiberNode Tree的结构。
下一节主要介绍Fiber作为Reconciler核心模块的工作单元,是如何创建及更新DOM的。