React 的渲染流程从虚拟 DOM 树的生成到真实 DOM 的挂载和更新是一个层层递进的过程。以下是详细的解析:
渲染流程概述
React 的渲染流程可以分为两个阶段:
- 初次渲染(Mounting): 将虚拟 DOM 树转换为真实 DOM,并挂载到页面。
- 更新渲染(Updating): 比较新旧虚拟 DOM 树(Diff),仅更新需要变更的部分。
这两个阶段的流程如下:
一、初次渲染(Mounting)
- 创建组件树并生成虚拟 DOM- 当 React 应用启动时,调用
React.createRoot(container).render(<App />)
。- React 递归调用组件树的render
方法或function
,生成一个完整的虚拟 DOM 树。- 每个组件都会返回一个表示 UI 的虚拟 DOM 节点。示例:function App() { return ( <div> <h1>Hello, React!</h1> <p>Welcome to the world of React.</p> </div> );}
虚拟 DOM 树:{ type:'div', props:{ children:[{ type:'h1', props:{ children:'Hello, React!'}},{ type:'p', props:{ children:'Welcome to the world of React.'}}]}}
- 调和(Reconciliation)- React 将虚拟 DOM 树与当前页面的真实 DOM 进行比较(此时页面为空)。- 因为页面中没有 DOM,React 将虚拟 DOM 直接转化为真实 DOM。
- 生成真实 DOM 并挂载- React 遍历虚拟 DOM 树,使用
document.createElement
创建真实 DOM 节点。- 为每个节点设置属性(如className
、id
等),并递归插入子节点。- 最终将构建好的 DOM 树挂载到指定的容器中。示例挂载代码:const container = document.getElementById('root');ReactDOM.createRoot(container).render(<App />);
二、更新渲染(Updating)
当组件的
state
或
props
发生变化时,React 会重新渲染受影响的部分。此过程包括以下步骤:
1. 检测变化
- 组件的状态(
state
)或属性(props
)更新时,React 会触发组件的更新。 - 调用组件的
render
方法或function
,生成新的虚拟 DOM 树。
示例:
function Counter() {
const [count, setCount] = React.useState(0);
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
- 初始虚拟 DOM:
{ type:'div', props:{ children:[{ type:'p', props:{ children:0}},{ type:'button', props:{ children:'Increment', onClick:[Function]}}]}}
- 当点击按钮后,
count
变为1
,生成新的虚拟 DOM:{ 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 直接移除旧节点并插入新节点。- 如果类型相同,比较属性和子节点。 - 属性比较:- 比较新旧节点的属性,仅更新有变化的属性。示例:
Old:<div id="old"/>New:<div id="new"/>
React 仅更新id
为"new"
。 - 子节点比较:- React 使用
key
属性优化动态子节点的比较。- 如果没有key
,React 按序逐一比较,可能导致多余的重建。
3. 应用变化(Patch)
- React 计算出需要的最小 DOM 操作(增、删、改、移)。
- 批量更新 DOM,减少重绘和重排的次数。
示例:
- 假设旧 DOM 是:
<div><p>0</p><button>Increment</button></div>
- 新虚拟 DOM 变为:
<div><p>1</p><button>Increment</button></div>
- React 会:1. 更新
<p>
元素的文本内容从0
改为1
。2. 不会重新创建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 不会跨层级比较节点。比如,新旧虚拟 DOM 树中不同层级的节点不会被直接比较。
Diff 算法的过程
Diff 算法分为以下几个步骤:
1. 树的层次比较
- React 会逐层比较新旧虚拟 DOM 树,而不会跨层级进行。
- 如果根节点类型不同,直接替换整个子树。
示例:
旧虚拟 DOM:
<div>
<p>Old</p>
</div>
新虚拟 DOM:
<span>
<p>New</p>
</span>
结果:
div
被替换为
span
,整个子树重新创建。
2. 同类型节点的属性比较
- 对于同类型的节点(如两个
div
),React 会逐个比较其属性,并更新有变化的部分。
示例:
旧节点:
<div id="old" className="foo"></div>
新节点:
<div id="new" className="foo"></div>
结果:只更新
id
属性。
3. 子节点的比较
子节点的比较是 Diff 算法的核心。React 会根据子节点是否有
key
来采取不同的策略。
**3.1 无
key
的子节点**
- React 逐一比较新旧子节点的位置: 1. 如果新节点和旧节点的类型相同,递归比较它们的属性和子节点。2. 如果类型不同,移除旧节点并插入新节点。
示例:
旧子节点:
<ul>
<li>Apple</li>
<li>Banana</li>
</ul>
新子节点:
<ul>
<li>Banana</li>
<li>Apple</li>
</ul>
结果:React 假定第一个节点由
Apple
变为
Banana
,第二个由
Banana
变为
Apple
,导致两个节点被完全重建。
**3.2 有
key
的子节点**
key
提供了稳定的标识,React 可以通过key
快速找到对应节点,避免不必要的重建。
示例:
旧子节点:
<ul>
<li key="a">Apple</li>
<li key="b">Banana</li>
</ul>
新子节点:
<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 能够在性能与灵活性之间取得良好的平衡。
版权归原作者 GISer_Jinger 所有, 如有侵权,请联系我们删除。