知其所以然!探索 React 渲染工作原理 🔍
对于 React 开发者而言,仅仅掌握 React 的概念和特性是基础,但这远远不够。要想精通 React,我们必须深入挖掘其背后的原理。只有真正理解其工作机制,我们才能在开发中充分发挥这一框架的潜力和效率。本文将带领大家一探究竟,从 React 组件到最终渲染至浏览器屏幕上是在内部如何完成的,虽然不是 React 内部实现 100% 准确的描述,但希望你在阅读完本文后能够对 React 的渲染原理有更深入的理解。
一、概述
渲染简单来说可以分为四个阶段:
- 渲染被触发执行(通常都是由状态更新所触发的)
- 渲染阶段 React 会调用组件函数并确定 DOM 应该如何更新
- 提交阶段 DOM 会被更新以反映当前应用程序状态的变化
- 浏览器会将更新的 DOM 绘制到屏幕上
二、渲染触发(Render is Triggered)
组件渲染被触发只会发生在两种场景下:
- 应用程序第一次运行(initial render)
- 组件实例的状态更新(re-render)
注意,状态发生更新后,渲染并不会立即触发,而是在 JavaScript 引擎有“空闲时间”时被安排执行,这个差异通常只有几毫秒,所以我们无法察觉。此外,我们在一个函数中调用多个 setState 更新状态时,渲染会被批量执行,而不是独立执行所有的渲染。
三、渲染阶段(Render Phase)
在渲染阶段开始时,React 首先会遍历整个组件树,确定哪些组件实例需要重新渲染。对于需要重新渲染的组件,React 会调用它们的组件函数或类组件的 render 方法。这个过程会创建出新的 React 元素,这些元素共同构成了一个新的虚拟 DOM 树。这个新的虚拟 DOM 树代表了组件在更新后应有的内容。接下来,React 会进行调和(Reconciliation),也就是比较新的虚拟 DOM 树和之前的状态更新前的 Fiber 树。这个过程的目标是找出两者之间的差异,并决定如何最小化对真实 DOM 进行更改。调和的结果是得到一个更新后的 Fiber 树,其中包含了所有必要的一系列 DOM 操作,包括插入、删除、更新 DOM 节点等。整个渲染阶段的核心是调和过程以及其中的比对方法,接下来详细介绍这两部分内容。
3.1 调和(Reconciliation)
当状态发生变化时,我们为什么不直接更新重建整个 DOM 树反而需要调和这样的机制呢?我们都知道直接操作 DOM 的成是时远高于操作由 JavaScript 对象表示的虚拟 DOM 的,并且状态发生改变时并不是整个 DOM 都需要发生变化,往往只是一小部分内容需要更新,因此没有必要从头开始重新创建整个 DOM 元素,通过调和机制,我们可以决定哪些 DOM 元素实际上需要被插入、删除或更新的过程,以反映最新的状态变化,同时尽可能多地复用现有的 DOM。调和过程由 React 的 Fiber 架构处理,可以说它是 React 的引擎或核心。在应用程序的初始化渲染期间,React 会获取整个 React 元素树,并基于它构建一个 Fiber 树。与 React 元素树不同,Fiber 树在每次渲染时不会被重新创建,它的结构是持久的,但其内容会在调和过程中不断变化。Fiber 树是跟踪组件状态(state)、属性(props)、副作用(side effects)和使用的 hooks 等内容的最佳对象。组件实例的状态和属性在内部都存储在 Fiber 树中相应的 Fiber 节点中。此外,Fiber 节点还包含了一个要执行的工作队列,例如更新状态、更新引用、运行注册的副作用、执行 DOM 更新等。因此,Fiber 也可以被定义为工作单元(unit of work)。Fiber 的一个关键特性是它可以异步执行工作。这意味着渲染过程可以拆分成多个部分,可以对任务进行优先级排序,并且可以暂停、重复使用或丢弃工作。这种灵活性使得 React 能够更高效地处理复杂的更新和渲染任务,特别是在大型应用中。
那么,Fiber 具体是如何运作的呢?以上图为例,假设有一个 App 组件,其中包含一个 Modal 组件,当 showModal 属性设置为 true 时,Modal 组件会被渲染。当 showModal 更新为 false 时,会触发重新渲染,创建一个新的虚拟 DOM。在这个新的虚拟 DOM 中,Modal 组件及其子组件不再存在。这个新的虚拟 DOM 会与之前的 Fiber 树进行调和,在这个过程中,React 会逐步遍历整个树,根据新的虚拟 DOM 准确分析出当前 Fiber 树和更新后的 Fiber 树之间需要更改哪些内容。更新后的 Fiber 树记录了应该执行的变更,例如,如果 Btn 组件的文本需要更新,Modal 组件及其子组件对应的 DOM 应该被删除,而 Video 组件根据调和的结果,其 DOM 不需要发生任何变化。比对过程称为 Diffing,它是调和过程中的一个关键步骤,用于比较新旧虚拟 DOM 树,并确定最小的 DOM 更新操作。Diffing 算法的工作原理将在下一节中详细讨论。
3.2 比对(Diffing)
比对(Diffing)是调和过程中的一个关键步骤,它涉及根据元素在树中的位置逐一进行比较。Diffing 算法基于两个基本假设:1. 不同类型的元素(例如,一个<div>
和一个<header>
)会产生不同的树;2. 具有稳定 key 属性的元素在渲染过程中可以保持稳定。Diffing 算法主要处理两种不同的情况:
1、SAME POSITION, DIFFERENT ELEMENT:在这种情况下,React 树中相同位置上的新旧元素的类型是不同的。这意味着旧元素及其子元素将不再有效,React 会销毁旧组件(包括其状态)并从 DOM 中删除它们。如上图所示,如果一个<div>
被替换为一个<header>
,那么<div>
及其子元素 SearchBar 组件将被删除,并重新创建<header>
及其子元素 SearchBar 组件,此时 SearchBar 组件是被重新生成的,其状态已经发生重置。
2、SAME POSITION, SAME ELEMENT:如果树中相同位置上的元素类型相同,React 会保留这些元素在 DOM 中,包括它们的子元素和组件状态。如果元素的 props 或 attributes 发生变化,React 会更新这些属性,但不会重建组件或重置其状态。这种方式可以提高性能,因为避免了不必要的组件重建。如果我们希望在更新时创建一个具有全新状态的组件实例,我们可以利用key属性。当组件的key发生变化时,React会将其视为新元素,并销毁旧组件,然后创建一个新的组件实例,这样新组件就会有一个全新的状态。
四、提交阶段(Commit Phase)
提交阶段是 React 更新 DOM 的最终阶段,它涉及执行在渲染阶段确定的所有 DOM 更新操作,包括插入、删除和更新 DOM 元素。在这个阶段,React 会遍历渲染阶段生成的效果列表,这个列表详细记录了为了使 DOM 与最新的虚拟 DOM 树保持一致所需要执行的所有操作。然后,React 将这些操作应用于 DOM 树中的现有 DOM 元素。与渲染阶段可能的异步执行不同,提交阶段是同步执行的,不能被中断。这意味着 DOM 更新要么全部完成,要么完全不执行,从而确保 UI 始终与应用的状态同步,不会出现显示部分更新结果的情况。一旦提交阶段完成,之前用于渲染的 workInProgress Fiber 树就会变成当前状态对应的已确认的 Fiber 树,为下一个渲染周期做好准备。
值得注意的是,React 库本身并不直接与 DOM 元素交互。React 库负责渲染阶段,而提交阶段的工作是由特定的平台适配器来完成的。在 Web 应用程序中,这个适配器是 react-dom。这是因为 React 设计为一个跨平台的库,可以运行在不同的平台上,而不仅仅是 Web,除了 react-dom 之外,例如我们可以使用 react-native 库构建 ios/android 应用程序......
五、总结
- React 的渲染流程始于渲染触发,这通常有两种情况:应用程序的初始渲染和组件实例状态的改变。一旦渲染被触发,流程便进入渲染阶段。
- 在渲染阶段,虽然不会立即反映在视觉上,但 React 会重新调用所有需要重新渲染的组件实例的组件函数。这个过程会创建一个或多个 React 元素,并构建出一个新的虚拟 DOM,即一棵由 React 元素构成的树。值得注意的是,如果一个组件被重新渲染,其所有子组件也会随之重新渲染。这是 React 确保整个组件树与状态保持一致的方式。接下来,新的虚拟 DOM 需要与当前的 Fiber 树进行调和。这个过程至关重要,因为直接销毁和重建整个 DOM 树将非常低效。通过调和,React 能够确定屏幕上需要更新的最小数量的 DOM 元素,从而提高效率并最大程度地复用现有的 DOM 元素。调和过程由 Fiber 调和器完成,它与 Fiber 树协同工作。在 Fiber 树中,每个 React 元素和 DOM 元素都对应一个 Fiber 节点,这些节点保存了组件的状态、props、副作用、使用的 hooks 以及工作队列等信息。调和执行完成后,工作队列中会包含该元素所需的 DOM 更新。渲染阶段的最终输出是更新后的 Fiber 树以及一个包含所有需要执行的 DOM 更新的列表。重要的是,渲染阶段是异步执行的,Fiber 架构能够将工作拆分成多个小块,并能够暂停和恢复工作,这为 React 提供了更高的灵活性和性能优化。
- 随后,DOM 更新列表将在提交阶段被应用到 DOM。在这个阶段,像 react-dom 这样的渲染器将执行实际的 DOM 操作,如插入、删除或更新 DOM 元素,以确保 DOM 反映了当前状态的更新。与渲染阶段的异步执行不同,提交阶段是同步的,所有 DOM 更新都是一次性执行的,这确保了 UI 随着时间推移保持一致。通过分离渲染和提交阶段,React 能够提供高性能和高可扩展性的用户界面开发体验。提交阶段的同步执行确保了 UI 的一致性和稳定性,而渲染阶段的异步处理则允许 React 优化和批量处理更新,从而提高应用的性能。
- 最终,浏览器会发现 DOM 已经更新,并会重新绘制到屏幕上,至此完成了从 React 元素的更新到 DOM 的最终更新和浏览器屏幕上的绘制的完整流程。
附录:相关概念
1. 虚拟 DOM(React Element Tree)
虚拟 DOM,即“Tree of all React elements created from all instances in the component tree”。在初始渲染时,React 构建整个组件树,并将其转换为一个庞大的 React 元素集合,我们称之为虚拟 DOM。从本质上看,它是一个 JavaScript 对象。如上图所示,当组件 D 的状态发生变化时,会触发重新渲染。此时,React 会重新调用 D 组件的函数,并将新的 React 元素更新到 React 元素树中。值得注意的是,无论传入的 props 是否改变,当一个组件渲染时,其所有子组件都会被重新渲染。从图中可以看出,两个组件 E 的 React 元素也被重新生成。同理,如果组件 A 发生变化,那么整个应用程序的组件树都会重新渲染。React 采用这种策略是因为它无法预知组件更新是否会影响其子组件,本着谨慎的原则,React 会重新渲染所有内容。当然,这种渲染并不是实际的 DOM 操作,因此其性能开销是相对较小的。
2. 纤维树(Fiber Tree)
Fiber Tree 是 React 16 及之后版本中引入的一个核心概念,它是 React 内部用于表示组件树的数据结构,它为我们渲染的每个 React 元素创建纤维节点,然后将其连接到其父节点、子节点和兄弟节点以创建完整的树。从上图中我们可以看出,Fiber Tree 可以看成是一个链表,每个父节点都包含指向其第一个(即最左侧)子节点的链接,但不包含指向其他子节点的链接,同时每个子节点又与它的兄弟姐妹链接在一起。Fiber Tree 是整个 DOM 结构的完整表示,而不仅仅是 React 元素,如上图所示,Fiber Tree 不仅包含 React 元素,还包含了常规 DOM 元素,如 h3/button。
转载自:https://juejin.cn/post/7375048434128044067