React 的渲染流程从虚拟 DOM 树的生成到真实 DOM 的挂载和更新是一个层层递进的过程。以下是详细的解析:
渲染流程概述
React 的渲染流程可以分为两个阶段:
- 初次渲染(Mounting): 将虚拟 DOM 树转换为真实 DOM,并挂载到页面。
- 更新渲染(Updating): 比较新旧虚拟 DOM 树(Diff),仅更新需要变更的部分。
这两个阶段的流程如下:
一、初次渲染(Mounting)
-
创建组件树并生成虚拟 DOM
- 当 React 应用启动时,调用
React.createRoot(container).render(<App />)
。 - React 递归调用组件树的
render
方法或function
,生成一个完整的虚拟 DOM 树。 - 每个组件都会返回一个表示 UI 的虚拟 DOM 节点。
示例:
jsxfunction App() { return ( <div> <h1>Hello, React!</h1> <p>Welcome to the world of React.</p> </div> ); }
虚拟 DOM 树:
javascript{ type: 'div', props: { children: [ { type: 'h1', props: { children: 'Hello, React!' } }, { type: 'p', props: { children: 'Welcome to the world of React.' } } ] } }
- 当 React 应用启动时,调用
-
调和(Reconciliation)
- React 将虚拟 DOM 树与当前页面的真实 DOM 进行比较(此时页面为空)。
- 因为页面中没有 DOM,React 将虚拟 DOM 直接转化为真实 DOM。
-
生成真实 DOM 并挂载
- React 遍历虚拟 DOM 树,使用
document.createElement
创建真实 DOM 节点。 - 为每个节点设置属性(如
className
、id
等),并递归插入子节点。 - 最终将构建好的 DOM 树挂载到指定的容器中。
示例挂载代码:
javascriptconst container = document.getElementById('root'); ReactDOM.createRoot(container).render(<App />);
- React 遍历虚拟 DOM 树,使用
二、更新渲染(Updating)
当组件的 state
或 props
发生变化时,React 会重新渲染受影响的部分。此过程包括以下步骤:
1. 检测变化
- 组件的状态(
state
)或属性(props
)更新时,React 会触发组件的更新。 - 调用组件的
render
方法或function
,生成新的虚拟 DOM 树。
示例:
jsx
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
-
初始虚拟 DOM:
javascript{ type: 'div', props: { children: [ { type: 'p', props: { children: 0 } }, { type: 'button', props: { children: 'Increment', onClick: [Function] } } ] } }
-
当点击按钮后,
count
变为1
,生成新的虚拟 DOM:javascript{ type: 'div', props: { children: [ { type: 'p', props: { children: 1 } }, { type: 'button', props: { children: 'Increment', onClick: [Function] } } ] } }
2. 比较新旧虚拟 DOM(Diff)
React 使用 Diff 算法 比较新旧虚拟 DOM 树,找出变化部分。
关键步骤:
-
类型比较:
- 如果节点类型(如
div
和span
)不同,React 直接移除旧节点并插入新节点。 - 如果类型相同,比较属性和子节点。
- 如果节点类型(如
-
属性比较:
- 比较新旧节点的属性,仅更新有变化的属性。
示例:
javascriptOld: <div id="old" /> New: <div id="new" />
React 仅更新
id
为"new"
。 -
子节点比较:
- React 使用
key
属性优化动态子节点的比较。 - 如果没有
key
,React 按序逐一比较,可能导致多余的重建。
- React 使用
3. 应用变化(Patch)
- React 计算出需要的最小 DOM 操作(增、删、改、移)。
- 批量更新 DOM,减少重绘和重排的次数。
示例:
-
假设旧 DOM 是:
html<div> <p>0</p> <button>Increment</button> </div>
-
新虚拟 DOM 变为:
html<div> <p>1</p> <button>Increment</button> </div>
-
React 会:
- 更新
<p>
元素的文本内容从0
改为1
。 - 不会重新创建
button
元素。
- 更新
三、Fiber 架构
在 React 16+ 中,更新阶段由 Fiber 架构 驱动,以提高性能。
1. Fiber 的作用
- 将渲染工作分解为多个小任务(可中断的工作单元)。
- 实现优先级机制,高优先级任务(如用户交互)可打断低优先级任务(如后台渲染)。
2. Fiber 渲染流程
-
渲染阶段(Render Phase): 构建新的 Fiber 树,比较新旧 Fiber 树,计算出需要的更新。
- 此阶段是纯粹的计算,不会直接操作 DOM。
- 可中断,React 会优先处理高优先级任务。
-
提交阶段(Commit Phase): 将更新应用到真实 DOM。
- 这是不可中断的过程,React 将计算出的差异(Patch)批量提交到 DOM。
总结
React 的渲染流程从虚拟 DOM 到真实 DOM,大致可以分为以下步骤:
-
初次渲染:
- 生成虚拟 DOM 树。
- 调和并挂载真实 DOM。
-
更新渲染:
- 检测状态或属性变化。
- 生成新虚拟 DOM 树。
- Diff 算法比较新旧虚拟 DOM,计算最小变更。
- 使用 Fiber 提高性能,将更新应用到真实 DOM。
这种分阶段的设计使得 React 能够高效地渲染和更新 UI,同时保持良好的用户体验。
React 的 Diff 算法 是其核心性能优化技术,用于比较新旧虚拟 DOM 树的差异,并以最小的代价更新真实 DOM。为了保证效率,React 并没有采用传统的暴力对比方法(时间复杂度为 (O(n^3))),而是结合特定的假设对 Diff 过程进行优化,将时间复杂度降低到 (O(n))。
以下是 Diff 算法的详解:
Diff 算法的优化假设
React 的 Diff 算法基于以下三个假设来简化比较过程:
-
不同类型的节点产生完全不同的树
- 如果两个节点类型(如
div
和span
)不同,React 会直接销毁旧节点及其子节点,重新创建新节点,而不是逐一比较子节点。
- 如果两个节点类型(如
-
开发者可通过唯一的
key
标识子节点- 当比较同一层级的子节点时,React 假定它们的顺序可能会改变,因此会利用
key
来快速定位变化节点。
- 当比较同一层级的子节点时,React 假定它们的顺序可能会改变,因此会利用
-
同级子节点只与同级的其他子节点进行比较
- React 不会跨层级比较节点。比如,新旧虚拟 DOM 树中不同层级的节点不会被直接比较。
Diff 算法的过程
Diff 算法分为以下几个步骤:
1. 树的层次比较
- React 会逐层比较新旧虚拟 DOM 树,而不会跨层级进行。
- 如果根节点类型不同,直接替换整个子树。
示例:
旧虚拟 DOM:
jsx
<div>
<p>Old</p>
</div>
新虚拟 DOM:
jsx
<span>
<p>New</p>
</span>
结果:div
被替换为 span
,整个子树重新创建。
2. 同类型节点的属性比较
- 对于同类型的节点(如两个
div
),React 会逐个比较其属性,并更新有变化的部分。
示例:
旧节点:
jsx
<div id="old" className="foo"></div>
新节点:
jsx
<div id="new" className="foo"></div>
结果:只更新 id
属性。
3. 子节点的比较
子节点的比较是 Diff 算法的核心。React 会根据子节点是否有 key
来采取不同的策略。
3.1 无 key
的子节点
- React 逐一比较新旧子节点的位置:
- 如果新节点和旧节点的类型相同,递归比较它们的属性和子节点。
- 如果类型不同,移除旧节点并插入新节点。
示例:
旧子节点:
jsx
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
新子节点:
jsx
<ul>
<li>Banana</li>
<li>Apple</li>
</ul>
结果:React 假定第一个节点由 Apple
变为 Banana
,第二个由 Banana
变为 Apple
,导致两个节点被完全重建。
3.2 有 key
的子节点
key
提供了稳定的标识,React 可以通过key
快速找到对应节点,避免不必要的重建。
示例:
旧子节点:
jsx
<ul>
<li key="a">Apple</li>
<li key="b">Banana</li>
</ul>
新子节点:
jsx
<ul>
<li key="b">Banana</li>
<li key="a">Apple</li>
</ul>
结果:
- React 检测到
key="b"
的节点仍存在,只需更新位置。 - 同样,
key="a"
的节点只更新位置。
这种方式避免了重建节点,仅调整顺序。
4. 插入、删除、移动
在比较过程中,React 会记录以下操作:
- 插入新节点:当新节点在旧节点中不存在时,React 会创建并插入它。
- 删除旧节点:当旧节点在新节点中不存在时,React 会删除它。
- 移动节点 :当
key
相同但位置变化时,React 会调整节点位置。
操作流程:
- 生成差异的补丁(Patch)。
- 最小化 DOM 操作,应用这些补丁。
Diff 算法的实现机制
React 的 Diff 算法是基于以下过程实现的:
-
单节点 Diff
- 比较节点类型,相同则继续比较属性和子节点。
- 不同则替换节点。
-
多节点 Diff
- 如果有
key
,通过key
映射对子节点进行高效比较。 - 如果无
key
,按顺序比较,可能导致不必要的重建。
- 如果有
-
最小化操作
- React 会批量执行必要的 DOM 更新,减少浏览器的重绘和重排次数。
关键优化点
-
Key 的使用:
- 在动态列表中,使用唯一的
key
,可显著提高 Diff 效率。 - 不推荐使用索引作为
key
,因为顺序改变时可能导致不必要的更新。
- 在动态列表中,使用唯一的
-
分层比较:
- React 限制比较在同一层级进行,避免了跨层级的复杂计算。
-
批量更新:
- React 使用批处理(Batching)技术,将多次状态更新合并为一次操作,降低性能开销。
总结
React 的 Diff 算法是为了高效地更新真实 DOM,遵循以下原则:
- 逐层比较,避免跨层级复杂度。
- 利用
key
优化动态子节点的对比。 - 尽量减少实际的 DOM 操作。
通过这些优化策略,React 能够在性能与灵活性之间取得良好的平衡。