0.Overview
步骤 1: 触发一次渲染
初次渲染
当应用启动时,会触发初次渲染。它是通过调用目标 DOM 节点的 createRoot,然后用你的组件调用 render
函数完成的:
//index.js
import Image from './Image.js';
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'))
root.render(<Image />);
//Image.js
export default function Image() {
return (
<img
src="https://i.imgur.com/ZF6s192.jpg"
alt="'Floralis Genérica' by Eduardo Catalano: a gigantic metallic flower sculpture with reflective petals"
/>
);
}
状态更新时重新渲染
一旦组件被初次渲染,你就可以通过使用 set 函数 更新其状态来触发之后的渲染。更新组件的状态会自动将一次渲染送入队列。
步骤 2: React 渲染你的组件
在进行初次渲染时, React 会调用根组件。
对于后续的渲染, React 会调用内部状态更新触发了渲染的函数组件。
这个过程是递归的:如果更新后的组件会返回某个另外的组件,那么 React 接下来就会渲染 那个 组件,而如果那个组件又返回了某个组件,那么 React 接下来就会渲染 那个 组件,以此类推。这个过程会持续下去,直到没有更多的嵌套组件并且 React 确切知道哪些东西应该显示到屏幕上为止。
步骤 3: React 把更改提交到 DOM 上
在渲染(调用)你的组件之后,React 将会修改 DOM。
对于初次渲染, React 会使用 appendChild() DOM API 将其创建的所有 DOM 节点放在屏幕上。
对于重渲染, React 将应用最少的必要操作(在渲染时计算!),以使得 DOM 与最新的渲染输出相互匹配。
React 仅在渲染之间存在差异时才会更改 DOM 节点。 例如,有一个组件,它每秒使用从父组件传递下来的不同属性重新渲染一次。注意,你可以添加一些文本到
<input>
标签,更新它的value
,但是文本不会在组件重渲染时消失:
React 的渲染和更新流程是其核心机制之一,它负责确保 UI 与应用状态保持一致。以下是详细的解析,包括渲染流程、更新流程,以及它们涉及的核心概念和机制。
一、React 渲染流程
当 React 应用第一次加载时,会经历以下的渲染流程:
1. 初次渲染
初次渲染是从根组件开始,递归构建整个组件树,生成 UI,直至页面呈现。
流程步骤:
-
创建 React 元素树: React 的
JSX
被转换成 JavaScript 对象(即 React 元素),这些元素描述了 UI 的结构。
-
构建 Fiber 树:
- React 会将 React 元素转换为内部的 Fiber 数据结构。
- Fiber 是 React 的核心工作单元,表示组件的信息和其状态。
-
Reconciliation (协调过程):
- React 开始从根组件递归遍历,调用组件的
render
方法,生成虚拟 DOM。 - 如果是函数组件,会直接执行该函数;如果是类组件,会实例化类,调用其
render
方法。
- React 开始从根组件递归遍历,调用组件的
-
Commit 阶段 (提交真实 DOM):
- 将生成的虚拟 DOM 转换为真实 DOM,并挂载到页面。
- 在这一阶段,React 会执行副作用(如
useEffect
或componentDidMount
)。
主要分为两部分:
-
Before Mutation
:更新前的一些操作(例如记录 DOM 状态)。 -
Mutation
:操作真实 DOM(如插入、更新或删除 DOM 节点)。
二、React 更新流程
当组件的 state
或 props
发生变化时,会触发更新流程。React 的更新流程主要包括以下步骤:
1. 触发更新
- 当组件调用
setState
或接收新的props
时,React 标记该组件为需要更新。
2. Scheduler 调度更新
- 调度优先级:React 根据任务的优先级(例如用户输入的更新比动画更紧急)决定何时开始工作。
- React 依赖 时间切片 (Time Slicing),将任务分成多个小块,并在浏览器空闲时执行。
3. Reconciliation (协调过程)
- 比较更新前后的 Fiber 树(即 Diff 算法)。
- 如果两个 Fiber 节点表示相同的元素,保留原有的 DOM 节点;如果不同,则生成新的 DOM 节点。
主要策略:
- 类型相同的节点:更新属性并复用 DOM 节点。
- 类型不同的节点:销毁旧节点,创建新节点。
-
列表的 Diff 算法:根据
key
属性优化节点的复用。
4. Commit 阶段
- 完成虚拟 DOM 的更新后,React 提交更改到真实 DOM。
- 执行 DOM 操作(如插入、删除、更新)。
- 执行副作用(如
useEffect
或componentDidUpdate
三、React 的核心内容详解
1. Fiber 架构
- Fiber 是 React 16 引入的调度算法,核心目标是实现 可中断的渲染。
- 每个组件对应一个 Fiber 节点,记录了组件的类型、状态、DOM 引用等。
特点:
- 可中断:React 将渲染分成多个任务,可以随时暂停或恢复。
- 优先级:不同任务有不同的优先级(如用户输入 > 数据加载)。
2. 时间切片 (Time Slicing)
- React 使用
requestIdleCallback
或MessageChannel
模拟时间切片。 - 当渲染任务较重时,React 会暂停渲染,将控制权交还给浏览器,保证界面流畅性。
3. Reconciliation (Diff 算法)
- React 使用了一种 O(n) 的算法,而不是传统虚拟 DOM 的 O(n^3)。
- 核心是通过
key
和节点类型快速判断节点是否需要更新。
4. Scheduler 调度
- React 内置 Scheduler,通过任务优先级和调度机制实现任务分配。
-
优先级分类:
-
Immediate
:同步任务(如setState
的同步模式)。 -
User-blocking
:如用户输入。 -
Normal
:常规更新。 -
Low
:非关键更新。 -
Idle
:空闲时执行的任务。
-
5. Hooks 的依赖收集
- 对于
useEffect
或useMemo
,React 会根据依赖数组[dependencies]
判断是否需要重新执行。
四、示例讲解:从渲染到更新
初次渲染示例
function App() {
const [count, setCount] = React.useState(0);
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
-
初次渲染:
- 调用
App
函数,生成虚拟 DOM。 - 构建 Fiber 树,将虚拟 DOM 提交为真实 DOM。
- 调用
-
点击更新:
- 点击按钮触发
setCount
,React 标记App
为需要更新。 - React 比较前后 Fiber 树,发现
h1
的内容需要更新。 - 提交更改,仅更新
h1
的内容,而不会重新创建整个 DOM。
- 点击按钮触发
五、优化点
-
避免不必要的更新:
- 使用
React.memo
和useMemo
缓存组件和计算结果。
- 使用
-
合理设置
key
:- 为列表的每个元素分配唯一且稳定的
key
,避免重复渲染。
- 为列表的每个元素分配唯一且稳定的
-
按需加载:
- 使用动态加载(如
React.lazy
)减小初次渲染的体积。
- 使用动态加载(如
———————————————————————————————————————————
六、值得关注的点
在React的渲染流程中,除了基本的流程框架外,还有一些细节值得关注,它们对于理解和优化React应用具有重要意义。以下是一些关键的细节:
1. 渲染流程的阶段划分
- Render阶段(协调阶段):这个阶段主要是进行虚拟DOM的比较和更新计划的制定。React会在这个阶段调用组件的render方法(或函数体),生成新的虚拟DOM,并与旧的虚拟DOM进行比较,找出差异点,并标记需要更新的Fiber节点。
- Commit阶段:这个阶段主要是将更新同步到实际的DOM中。React会在这个阶段执行DOM操作,例如创建、更新或删除DOM元素,以反映组件树的最新状态。
2. Fiber架构的引入
- React 16引入了Fiber架构,实现了异步可中断的渲染。Fiber将渲染工作分解为一个个小的任务单元,每个任务单元称为Fiber节点。
- 在渲染过程中,如果有更高优先级的任务出现(如用户交互),渲染可以被中断,优先处理高优先级任务。这使得React能够更好地响应用户操作,提供更流畅的用户体验。
3. 调度器(Scheduler)的作用
- 调度器负责为任务排序优先级,让优先级高的任务先进入到协调器进行处理。
- 调度器在浏览器的原生API中有类似的实现(如requestIdleCallback),但React团队为了实现更稳定和兼容的调度机制,自己实现了一套调度器。
4. 副作用(Effects)的处理
- 在React中,副作用是指那些会影响组件外部状态的操作,如数据获取、订阅事件、手动修改DOM等。
- 在Render阶段,React会收集这些副作用,并在Commit阶段执行它们。这样可以确保副作用在DOM更新之后执行,避免潜在的竞争条件。
5. 虚拟DOM的比较算法(Diff算法)
- React使用了一种高效的Diff算法来比较新旧虚拟DOM树的差异。
- 该算法采用了深度优先遍历和启发式算法等优化策略,以减少不必要的DOM操作和提高渲染性能。
6. 批量更新和异步执行
- React的setState机制是“批量更新”和“异步执行”的。
- 这意味着不会因为每个setState调用立即触发重新渲染,而是在所有更新完成后,进行一次统一的重新渲染。这有助于减少不必要的频繁渲染,提高应用的性能和响应性。
7. 错误边界(Error Boundaries)
- React提供了错误边界组件来捕获组件渲染过程中的错误。
- 错误边界组件是一个具有特定生命周期方法的类组件(如getDerivedStateFromError和componentDidCatch)。当子组件在渲染过程中抛出错误时,错误边界组件可以捕获该错误,并展示一个备用的UI,而不是让整个应用崩溃。
8. 渲染流程的中断和恢复
- Render阶段的工作流程是可以随时被打断的,原因可能包括有其他更高优先级的任务需要执行、当前的time slice没有剩余的时间等。
- 由于Render阶段的工作是在内存里面进行的,不会更新宿主环境UI,因此这个阶段即使工作流程反复被中断,用户也不会看到更新不完全的UI。
综上所述,React的渲染流程是一个复杂而高效的过程,涉及多个阶段和细节。通过深入理解这些细节,可以更好地优化React应用的性能和响应性,提高用户体验。