React Fiber
React Fiber
提出背景
浏览器渲染限制
浏览器的帧
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到 60 时,页面是流畅的,小于这个值时,用户会感觉到卡顿。
上图是一帧内需要完成的任务,浏览器在一帧内可完成如下任务:
输入事件捕捉:
- 浏览器首先会接受用户的输入事件,如点击、滚动、触摸等,并准备将其转换为相应的处理逻辑。
- Blocking input events(阻塞输入事件):例如
touch
或wheel
- Non-blocking input events(非阻塞输入事件):例如
click
或keypress
事件回调执行:
- 对于已注册的事件监听器,浏览器会执行相应的事件回调函数,处理用户输入或页面状态的变化。
帧开始(Begin frame):
- 每一帧事件(Per frame events),例如
window resize
、scroll
或media query change
样式计算和布局:
- rAF(requestAnimationFrame)
- 浏览器会解析和计算CSS样式,确定页面中每个元素的尺寸和位置。这个过程称为布局或重排。
绘制渲染(Paint):合成更新(Compositing update)、重绘部分节点(Paint invalidation)和 Record
绘制(渲染):
- 在确定了元素的样式和位置后,浏览器会开始绘制页面。
- 这包括将文本、图像和其他内容绘制到屏幕上。绘制操作可能涉及多个层级的渲染,最终合成到屏幕上显示。
合成:
- 如果页面使用了硬件加速(如CSS3D转换或WebGL),浏览器会将不同层级的渲染结果合成到一起,形成一个完整的页面图像。
空闲回调执行:如果在一帧的剩余时间内还有空闲时间,并且存在待执行的空闲回调(如RequestIdleCallback),浏览器会尝试执行这些回调。这些回调通常用于执行非关键性的、可延迟的任务。
浏览器的渲染过程受到设备刷新率的限制。一般来说,设备的刷新率是60Hz,意味着每秒钟有60帧的渲染机会。因此,浏览器需要在大约16.67毫秒(1000毫秒/60帧)内完成上述所有任务,以确保流畅的动画和用户交互。
- 浏览器的
JavaScript
引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待;如果JavaScript
线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿。 - 事件线程:当事件被触发时,对应的任务不会立刻被执行而是由事件线程把它添加到任务队列的末尾等待 JavaScript 的同步代码执行完毕后在空闲的时间里执行出队。
- 而浏览器的渲染引擎是单线程的,它将GUI描绘、时间器处理、事件处理、JavaScript执行、远程资源加载等任务放在一起执行。这意味着浏览器在处理某个任务时,必须等待该任务完成后才能开始下一个任务。
React16 之前架构问题
在React Fiber架构之前,也就是React 16之前的版本,其架构主要可以分为两个部分:Reconciler(协调器)和Renderer(渲染器)。
Reconciler(协调器)
它的主要任务是找出需要更新的组件。当组件的状态或属性发生变化时,Reconciler会开始工作。它会通过diff算法比较新旧组件树,找出差异,并生成一个更新列表。这个列表包含了所有需要更新的组件以及相应的更新操作。
Renderer(渲染器)
Renderer的任务是根据Reconciler生成的更新列表来更新DOM。它会遍历更新列表,对每个需要更新的组件执行相应的DOM操作。
栈调和机制下的 Diff 算法:
React的更新过程采用的是Stack架构,也就是循环递归方式,在这个过程中,Reconciler的工作是一个自顶向下的递归过程。一旦开始,它会持续占用主线程,直到所有的组件都更新完毕。如果在更新过程中遇到大量的计算或复杂的组件树,就可能导致主线程长时间被占用,从而阻塞页面的渲染和其他用户交互。
如下图:深度递归遍历对比节点过程中,调和器会重复“父组件调用子组件”的过程直到最深的一层节点更新完毕,才慢慢向上返回。该过程是同步的,不可以被打断。
Fiber架构的引入主要解决了React 16之前版本在性能上的一些主要问题:
- 长时间阻塞问题:
- 在传统的React同步渲染方式下,一旦开始渲染,就会一直执行到渲染完成或遇到I/O操作等阻塞任务。这可能导致页面在渲染过程中长时间处于空白或无响应状态,影响用户体验。
- Fiber架构通过将渲染过程拆分成多个小任务(即fiber),使得React可以根据优先级和时间片进行任务调度和暂停。这样,即使某个任务需要很长时间才能完成,也不会阻塞主线程,从而避免了长时间阻塞的问题。
- 优化用户界面的响应性能:
- 由于Fiber架构可以将渲染过程分解为多个小任务,并且可以根据任务优先级动态调度,因此能够更好地响应用户的交互操作。
- 在用户输入或动画等需要即时响应的场景下,Fiber可以优先处理高优先级任务,暂停或中断低优先级任务的执行,从而提高用户界面的响应性能和流畅度。
- 支持优先级调度:
- Fiber架构引入了任务优先级的概念,使得React可以根据任务的优先级来安排任务的执行顺序。这有助于确保关键任务能够优先得到处理,进一步提高应用的性能和用户体验。
什么是 Fiber?
Fiber 目的: 实现增量渲染。
可中断的工作单元
在传统上,React使用一种称为堆栈调和递归算法来处理虚拟DOM的更新。然而,这种方法在大型应用或频繁更新的情况下可能会产生性能问题。
为了解决这个问题,React Fiber引入了增量渲染的思想。它将更新任务分解成小的、可中断的单元,称为“fiber”。
因此 Fiber 代表一种 可中断的工作单元。
类似双向链表的数据结构
Fiber 的数据结构是一个链表结构,具体来说,它是一个双向链表。每个 Fiber 节点(FiberNode)都包含指向其子节点、兄弟节点以及父节点的指针,这些指针构成了整个 Fiber 链表。
FiberNode与 FiberTree
Fiber 机制下节点与树分别采用 FiberNode与 FiberTree 进行重构。
Fiber Node
Fiber 节点除了包含指向其他节点的指针外,还包含组件的类型、key、状态、属性、输入、输出等信息,以及任务的优先级、开始时间、结束时间、实际执行时间、是否过期、是否中断等任务调度相关的信息。这使得 React 可以方便地追踪组件的状态和变化,并根据需要执行相应的更新操作。
::: normal-demo FiberNode
{
// 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等
tag: WorkTag,
// ReactElement里面的key
key: null | string,
// ReactElement.type,调用`createElement`的第一个参数
elementType: any,
// The resolved function/class/ associated with this fiber.
// 表示当前代表的节点类型
type: any,
// 表示当前FiberNode对应的element组件实例
stateNode: any,
// 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
return: Fiber | null,
// 指向自己的第一个子节点
child: Fiber | null,
// 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
sibling: Fiber | null,
index: number,
ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject,
// 当前处理过程中的组件props对象
pendingProps: any,
// 上一次渲染完成之后的props
memoizedProps: any,
// 该Fiber对应的组件产生的Update会存放在这个队列里面
updateQueue: UpdateQueue<any> | null,
// 上一次渲染的时候的state
memoizedState: any,
// 一个列表,存放这个Fiber依赖的context
firstContextDependency: ContextDependency<mixed> | null,
mode: TypeOfMode,
// Effect
// 用来记录Side Effect
effectTag: SideEffectTag,
// 单链表用来快速查找下一个side effect
nextEffect: Fiber | null,
// 子树中第一个side effect
firstEffect: Fiber | null,
// 子树中最后一个side effect
lastEffect: Fiber | null,
// 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes
expirationTime: ExpirationTime,
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,
// fiber的版本池,即记录fiber更新过程,便于恢复
alternate: Fiber | null,
}
:::
Fiber Tree
Fiber Tree 的创建 :
React 在首次渲染(执行
ReactDOM.render
)时,会通过React.createElement
创建一颗 Element 树,可以称之为 Virtual DOM Tree由于要记录上下文信息,加入了 Fiber,每一个 Element 会对应一个 Fiber Node;
当 React 开始渲染一个组件时,它会创建一个 Fiber 节点作为起点,然后递归地创建子节点的 Fiber 节点,直到遍历完整个组件树,最后形成 Fiber Tree。
每一个 Fiber Node 节点与 Virtual Dom 一一对应,所有 Fiber Node 连接起来形成 Fiber Tree, 是个单链表树结构,如下图所示:
Fiber Tree 通过节点保存与映射,便能够随时地进行停止和重启,这样便能达到实现 任务分割 的基本前提。
Fiber Tree 反映了用于渲染 UI 的应用程序的状态。这棵树通常被称为 current 树(当前树,记录当前页面的结构状态)。
更新过程 Fiber Tree 变化:
- 在后续的更新过程中(
setState
),每次重新渲染都会重新创建 Element; - 但是 Fiber 不会,Fiber 只会使用对应的 Element 中的数据来更新自己必要的属性。
这个过程在 Fiber 出现之前的协调器 (称为 Stack Reconciler)是采取自顶向下递归比较来实现的,所以 无法中断这个递归比较的过程(持续占用主线程),这样主线程上的布局、动画等周期性任务以及交互响应就无法立即得到处理,影响用户体验。
在 React v18 将调度算法进行了重构,将之前的 Stack Reconciler 重构成新版的 Fiber Reconciler,变成了具有链表和指针的 单链表树遍历算法。通过指针映射,每个单元都记录着遍历当下的上一步与下一步,从而使遍历变得可以被暂停和重启。
Fiber 数据结构设计的目的:Fiber 数据结构的设计使得 React 的渲染过程可以中断和恢复。
- 在渲染过程中,如果主线程需要处理其他高优先级的任务或者达到了一定的执行时间,React 可以中断当前的渲染任务,保存当前节点的状态,然后在下次有空闲时间时恢复执行。
- 通过这种方式,Fiber 数据结构使得 React 可以更好地利用主线程的时间片,实现增量渲染和合作式调度,从而提升应用的性能和响应性。同时,由于 Fiber 数据结构是可复用的,React 可以在多个渲染任务之间共享节点,进一步减少内存消耗。
Fiber 思想
Fiber架构的核心思想是将渲染更新过程拆分成多个子任务,每次只做一小部分,然后检查是否还有剩余时间:
- 如果有剩余时间,就继续执行下一个任务;
- 如果没有,就挂起当前任务,将时间控制权交回给主线程,等待主线程空闲时再继续执行。
这种策略称为合作式调度(Cooperative Scheduling),它使得React的渲染过程可以中断和恢复,从而实现更好的任务调度、优先级管理和增量更新。
特征:
- 增量渲染(把渲染任务拆分成块,均匀分布到多帧)
- 更新时能够暂停、终止、复用渲染任务
- 给不同类型的更新赋予优先级
- 并发方面新的基础能力
增量渲染用来解决掉帧的问题,渲染任务拆分之后,每次只做一小段,做完一段就把时间控制权交还给主线程,而不像之前长时间占用。这种策略叫做 cooperative scheduling
(合作式调度),操作系统的 3 种任务调度策略之一(Firefox 还对真实 DOM 应用了这项技术)。
架构
React的架构主要由三个关键部分组成:调度器(Scheduler)、协调器(Reconciler)和渲染器(Renderer)。
- 调度器(Scheduler):
- 这是React架构的新增部分,它在React 16版本中被引入。
- 主要作用是管理任务的优先级和调度顺序。
- 调度器允许高优先级的任务优先进入协调器进行处理,确保这些任务能够及时得到关注和执行。
- 协调器(Reconciler):它的主要作用是通过Diff算法找出哪些组件发生了变化,找出差异,并生成一个更新列表。
- 渲染器(Renderer):
- 其主要职责是负责将协调器找出的需要更新的组件(更新列表)渲染到视图中。
- 不同的渲染器可以将组件渲染到不同的宿主环境的视图中
- 例如 React DOM会将组件渲染成HTML,而React Native则会渲染App的原生组件。
工作流程:
- 每个更新任务在 Scheduler 层都会被赋予一个优先级;
- 若发现 B 的优先级高于当前任务 A ,那么当前处于 Reconciler 层的 A 任务就会被中断;
- B渲染完成后,A 任务将会被重新推入 Reconciler层继续渲染,这便是所谓“可恢复’。
优先级调度实现(Scheduler)
优先级调度通常由调度器(Scheduler)完成,它允许 React 执行异步渲染,从而提高应用的性能和响应性。
以下是 React Fiber 架构中优先级调度实现的一些关键点:
- 任务拆分为 Work Units(工作单元): 在 Fiber 架构中,每个组件都被表示为一个 Fiber 对象。这些 Fiber 对象形成了一个链表,每个 Fiber 对象都包含了一个与之相关的任务或工作单元。这些工作单元可以被独立地执行、暂停和恢复。
- 优先级的概念: React 为每个任务分配了一个优先级。这些优先级可以是用户阻塞(user-blocking)、低优先级、普通优先级等。用户阻塞的任务(如用户交互后的反馈)通常会被赋予更高的优先级,以便尽快得到处理。
- 任务调度队列: React 使用了一个或多个任务调度队列来存储待处理的任务。这些队列根据任务的优先级进行排序。高优先级的任务会被放在队列的前面,以便在调度时能够优先得到处理。
- 请求空闲回调(requestIdleCallback): React 使用浏览器的
requestIdleCallback
API(如果可用)来调度任务。这个 API 允许 React 在浏览器的主线程空闲时执行低优先级的任务,从而不会阻塞用户交互或其他高优先级的任务。 - 中断和恢复: 如果在渲染过程中出现了高优先级的任务(例如,用户开始了一个新的交互),React 可以中断当前的渲染过程,转而处理高优先级的任务。当高优先级任务完成后,React 可以恢复之前中断的渲染过程。
- 时间分片(Time Slicing): React 利用时间分片技术,将长时间运行的任务拆分成多个较短的片段,并在多个浏览器帧中逐步完成。这有助于保持界面的响应性,防止长时间运行的任务阻塞浏览器的主线程。
任务优先级调度逻辑
变量解释:
startTime
: 任务的开始时间;expirationTime
:expirationTime
越小,任务的优先级就越高;timerQueue
: 一个以startTime
为排序依据的小顶堆,它存储的是startTime
大于当前时间(也就是待执行)的任务;taskQueue
: 一个以expirationTime
为排序依据的小顶堆它存储的是startTime
小于当前时间(也就是已过期)的任务;
逻辑描述:
- 创建
newTask
对象:- 当 React 需要调度一个任务(task)时,它会创建一个
newTask
对象。 - 这个对象包含了任务的相关信息,如执行的任务函数、优先级等。
- 当 React 需要调度一个任务(task)时,它会创建一个
- 检查
startTime
与currentTime
:- 调度器会检查
newTask
的startTime
属性是否大于当前的currentTime
。 startTime
是任务指定的执行开始时间,而currentTime
是当前的系统时间。
- 调度器会检查
- **未过期任务,加入
timerQueue
**:- 如果
startTime
未过期,即startTime
大于currentTime
,则认为这是一个未来的任务。 - 调度器会将这个任务加入到
timerQueue
中,这是一个按时间排序的任务队列。
- 如果
- 已过期任务,加入
taskQueue
:- 如果
startTime
已过期,即startTime
小于或等于currentTime
,则认为这是一个应立即执行的任务。 - 调度器会将这个任务加入到
taskQueue
中,这是一个包含已过期任务的队列。
- 如果
- 检查
taskQueue
是否为空:- 调度器会检查
taskQueue
是否为空,即是否还有未执行的任务。 - 如果
taskQueue
为空,说明当前没有待执行的任务。
- 调度器会检查
- 请求宿主环境回调 (
requestHostCallback
):- 如果
taskQueue
不为空,调度器会请求宿主环境(如浏览器)在合适的时机调用flushWork
函数。 flushWork
函数负责从taskQueue
中取出任务并执行它们。
- 如果
- 检查
newTask
是否是timerQueue
的堆顶任务:- 调度器会检查
newTask
是否是timerQueue
中堆顶的任务,即是否是下一个应该执行的任务。 - 这通常是通过比较
startTime
来确定的。
- 调度器会检查
- 请求宿主环境超时 (
requestHostTimeout
):- 如果
newTask
不是timerQueue
的堆顶任务,调度器会根据startTime
和currentTime
之间的差值请求一个超时回调。 - 当超时时间到达时,宿主环境会调用
handleTimeout
函数,该函数会从timerQueue
中取出并执行任务。
- 如果
- 返回
newTask
:- 无论
newTask
是即时任务还是延时任务,调度器都会返回newTask
对象,以便后续的执行和调度。
- 无论
通过这个优先级调度流程,React Fiber 的调度器能够灵活地处理各种任务,包括同步和异步任务,以及根据优先级和时间安排任务的执行。这种机制使得 React 能够更有效地利用时间片(time slices)来渲染组件,从而提供更流畅的用户体验。
协调器(Reconciler)
- 定义:协调器(Reconciler)是React中负责管理虚拟DOM树更新的关键部分,它负责比较新的虚拟DOM树(即将渲染的状态)和旧的虚拟DOM树(当前DOM在内存中的表示),然后计算出它们之间的差异,最后将这些差异应用到实际的DOM树上。
- 作用:协调器的主要作用是提高React应用的渲染性能、简化编程模型以及支持跨平台开发。
工作原理
- 构建虚拟DOM:当React组件的状态或属性发生变化时,React会重新调用组件的渲染函数,生成一个新的虚拟DOM树。这个树是一个轻量级的JavaScript对象,描述了组件在用户界面上的结构和样式。
- 差异比较:协调器会获取新的虚拟DOM树和旧的虚拟DOM树,然后使用一个高效的算法(如React的Diffing算法)来比较它们之间的差异。这个算法会尽可能地减少不必要的DOM操作,提高渲染性能。
- 生成更新列表:通过比较两个虚拟DOM树,协调器会生成一个更新列表,其中包含了需要应用到实际DOM上的所有更改。这个列表通常包括添加、删除、更新或移动DOM节点的指令。
- 应用更新:最后,协调器会将更新列表中的指令应用到实际的DOM树上,从而更新用户界面。这个过程通常是通过浏览器的DOM API来完成的。
特点与优势
- 提高性能:通过比较虚拟DOM树和生成更新列表,协调器能够减少不必要的DOM操作,从而显著提高React应用的渲染性能。
- 简化编程模型:React的声明式编程范式使得开发者只需要关注组件的状态和渲染逻辑,而不需要关心DOM操作的具体细节。协调器在背后默默地处理这些复杂的DOM操作,使得开发者能够更加专注于业务逻辑的实现。
- 支持跨平台:由于协调器处理的是虚拟DOM树而不是实际的DOM树,因此它可以很容易地扩展到其他平台(如React Native)。这使得开发者可以使用相同的React组件和逻辑来构建Web应用、移动应用或桌面应用。
渲染器(Renderer)
- 定义:渲染器是React框架中的一个核心组件,它负责将React元素(即虚拟DOM)转换为特定平台的实际UI。React是一个跨平台的库,这意味着它可以在多种环境中运行,如Web、移动(React Native)和桌面等。每种环境都有其特定的渲染方式,而渲染器就是负责这些转换的组件。
- 类型:React提供了多种渲染器,如ReactDOM(用于Web)、ReactNative(用于移动应用)和ReactART(用于渲染到Canvas、SVG或WebGL)。每种渲染器都实现了React的渲染接口,但具体实现细节因平台而异。
工作原理
- 虚拟DOM到实际UI的转换:当React组件的状态或属性发生变化时,React会重新渲染组件并生成一个新的虚拟DOM树。渲染器会接收到这个新的虚拟DOM树,并将其转换为特定平台的实际UI。
- 平台特定的实现:每种渲染器都有其特定的实现方式。
- 例如,ReactDOM会将虚拟DOM转换为浏览器可以理解的DOM节点,并更新到实际的DOM树中;(ReactDOM 渲染过程参考文章: 组件渲染流程)
- 而ReactNative则会将虚拟DOM转换为原生视图组件,并更新到移动应用的UI中。
- 可中断和恢复的渲染:React 18引入了并发模式(Concurrent Mode),这使得渲染器可以在渲染过程中暂停和恢复。这意味着当高优先级的任务出现时,渲染器可以中断当前任务并优先处理高优先级任务,从而提高应用的响应性和性能。
优势和特点
- 跨平台:由于渲染器是平台特定的,因此React可以轻松地在多种环境中运行,实现一次编写、到处运行的目标。
- 高性能:通过虚拟DOM和Diffing算法,React可以高效地比较新旧虚拟DOM树并计算出差异,然后只更新实际DOM中需要变化的部分,从而显著提高渲染性能。
- 并发模式:React 18的并发模式允许渲染器在等待异步操作(如数据获取)时暂停和恢复渲染,从而提供了更平滑的用户体验。此外,它还可以使开发者能够更精细地控制组件的更新优先级,进一步提升应用的响应性和性能。
- 自定义渲染器:React的渲染器接口是开放的,开发者可以根据需要实现自定义渲染器,将React元素渲染到任何他们想要的环境中。这为开发者提供了极大的灵活性和扩展性。
Fiber 对生命周期影响
Fiber 架构的重要特征就是可以被打断的异步渲染模式,根据“能否被打断”这一标准 React 16 的生命周期被划分为了渲染阶段( render) 和提交阶段(commit )两个阶段。
提交阶段(commit )又被细分为 Pre-commit 阶段和 Commit 阶段。
render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。
三个阶段
React的工作流程包括计算阶段、渲染阶段和提交阶段。
计算阶段:
- React会根据组件的更新优先级和调度策略,将工作单元分成多个批次进行处理。
- 计算阶段主要关注于创建更新、调度任务、构建Fiber树和识别差异。
渲染阶段:
- 纯净且没有副作用,可以被 React 暂停,中断或重新开始。这就导致 render 阶段的生命周期都是有可能被重复执行的。
- React会根据工作单元的类型和优先级,执行相应的渲染操作。
- 渲染阶段重点在于生成EffectList,即收集并准备需要应用到DOM的更改。
提交阶段:
- 提交阶段(commit )又被细分为 Pre-commit 阶段和 Commit 阶段。
- Pre-commit 阶段:可以读取 DOM;
- Commit 阶段:
- 可以操作 DOM,运行副作用和更新队列;
- React会将更新后的虚拟DOM节点映射到实际的DOM,更新用户界面。
React15 和 React16+ 架构对生命周期影响
从 render 到 commit 的过程:
大致工作步骤
React Fiber 的工作流程可以大致分为以下几个步骤:
- 任务生成与拆分:当组件的状态或属性发生变化时,React 会开始生成更新任务。与传统的 React 递归更新方式不同,Fiber 将这些更新任务拆分成多个小单元,每个单元对应一个 Fiber 节点。这些 Fiber 节点组成了一个任务队列,每个节点都代表了虚拟 DOM 树上的一个节点。
- 优先级调度:React 使用调度器来处理这些任务的优先级。调度器会根据任务的优先级、浏览器的空闲时间以及其他因素来决定任务的执行顺序。高优先级的任务会优先得到执行,而低优先级的任务则可能在浏览器空闲时执行,或者被更高优先级的任务中断。
- 执行与中断:当浏览器空闲时,React 会从任务队列中取出任务并执行。每个任务都是一个小的执行单元,它可能只更新虚拟 DOM 树的一小部分。在执行过程中,如果浏览器需要处理其他高优先级任务,React 会中断当前的执行,待浏览器空闲时再继续执行。这种中断和恢复的特性使得 React 能够更好地响应用户的操作,保持页面的流畅性。
- 协调与渲染:在执行任务的过程中,React 的协调器会负责处理组件更新的逻辑,包括构建 Fiber 树、执行更新、生成副作用等。当所有的任务都执行完毕后,React 会将更新应用到实际的 DOM 上,完成渲染过程。
这个流程是循环进行的,React 会不断地从任务队列中取出任务并执行,直到所有的任务都完成。同时,React 也会根据浏览器的空闲时间和任务的优先级来动态调整任务的执行顺序,以达到最优的性能和响应性。
详细步骤
从 JSX 代码到 ReactDOM.render
方法,再到渲染出虚拟 DOM,并最终渲染到真实 DOM 的整个过程,结合 React Fiber 的计算阶段、渲染阶段和提交阶段,详细流程如下:
1. JSX 转换与 ReactDOM.render
调用
- JSX 转换:
- 在编写 React 组件时,我们通常会使用 JSX 语法,它允许我们像写 HTML 一样写组件结构。
- 浏览器并不能直接理解 JSX,所以在构建过程中,JSX 会被 Babel 等工具转换成纯 JavaScript 代码,通常是使用
React.createElement
方法来创建 React 元素。
- ReactDOM.render 调用:
- 在应用的入口点,我们会调用
ReactDOM.render
方法,将根组件传递给这个方法,并指定一个 DOM 元素作为挂载点。ReactDOM.render
方法会启动 React 的整个渲染流程。
- 在应用的入口点,我们会调用
2. 计算阶段(Reconciliation)
构建 Fiber 树:
- 任务生成与拆分:
- React 会开始调度过程,创建根 Fiber 节点,并根据根组件生成对应的 Fiber 树。
- 每个 Fiber 节点都包含了组件的类型、属性、状态等信息,以及指向其子节点和父节点的指针。
- 这些 Fiber 节点组成了一个任务队列,每个节点都代表了虚拟 DOM 树上的一个节点。
- 比较与差异检测:
- 当组件的状态或属性发生变化时,React 开始进入计算阶段。
- React 会使用一种称为“协调”的过程,使用协调器(Reconciler)来比较新的 Fiber 树与旧的 Fiber 树(如果存在的话)。
- 这个过程中,React 会识别出哪些部分发生了改变,哪些部分保持不变,从而确定需要执行哪些更新。
- 优先级调度:
- React 使用调度器(Scheduler)来确定任务的执行顺序。
- 调度器会根据任务的优先级、浏览器的空闲时间以及其他因素来决定任务的执行顺序。
- 高优先级的更新(如用户交互引起的更新)会优先执行,而低优先级的更新(如数据获取后的更新)可能会被延迟。
3. 渲染阶段(Render)
生成副作用列表:
生成新的 Fiber 树:在渲染阶段,React 会根据计算阶段的结果生成一棵新的 Fiber 树。这棵新树反映了组件的新状态。
生成副作用列表:
在构建新树的过程中,React 会识别出需要应用到真实 DOM 上的更改,并将这些更改作为“副作用”记录下来。这些副作用可能包括添加、更新或删除 DOM 元素。
副作用列表 (Effect List) 可以理解为是一个存储
effectTag
副作用列表容器。它是由 Fiber 节点和指针nextEffect
构成的单链表结构,这其中还包括第一个节点firstEffect
,和最后一个节点lastEffect
。如下图所示:React 采用深度优先搜索算法,在
render
阶段遍历 Fiber 树时,把每一个有副作用的 Fiber 筛选出来,最后构建生成一个只带副作用的 Effect List 链表。
中断与恢复:渲染阶段是可中断的。如果浏览器需要处理其他高优先级任务,React 会中断当前的渲染过程,并在浏览器空闲时恢复执行。
4. 提交阶段(Commit)
将虚拟 DOM 渲染到真实 DOM:
- 应用副作用列表:
- 在提交阶段,React 会遍历副作用列表,并依次执行每个副作用。这通常包括创建、更新或删除 DOM 元素,从而将虚拟 DOM 的更改应用到真实的 DOM 上。
- 屏幕更新:一旦所有的副作用都被执行完毕,浏览器就会重新渲染页面,用户会看到更新后的内容。
- 清理工作:提交阶段结束后,React 会进行一些清理工作,比如释放不再需要的 Fiber 节点和内存资源,为下一次的更新做好准备。
5. 循环更新
这个过程是循环进行的。每当组件的状态或属性发生变化时,React 都会从计算阶段开始,重新构建 Fiber 树,生成新的虚拟 DOM,并最终将更改应用到真实的 DOM 上。同时,React 会根据任务的优先级和浏览器的空闲时间来动态调整执行顺序,以确保应用的响应性和性能。
通过这个流程,React 能够在保持高性能的同时,灵活地处理各种复杂的组件更新场景。Fiber 架构的引入使得 React 能够更好地利用浏览器的空闲时间,并在需要时中断和恢复更新过程,从而提高了应用的响应性和用户体验。