React 批量更新笔记
批量更新(Batching)是 React 的一项核心优化技术,它将多个状态更新合并为一次渲染,从而提升性能,理解批量更新机制,才写出更高效的 React.
1. 批量更新
批量更新是一种将多个状态更新合并为单次渲染的机制。考虑以下场景:
function Counter() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
function handleClick() {
setCount1(count1 + 1);
setCount2(count2 + 1);
// 两次状态更新,React 会合并为一次渲染
}
return (
<div>
<p>Count 1: {count1}</p>
<p>Count 2: {count2}</p>
<button onClick={handleClick}>增加</button>
</div>
);
}
点击按钮后,虽然调用了两次 setState,但组件只会重新渲染一次。这就是批量更新的效果。
2. React 17 及更早版本的批量更新
2.1 实现原理
// React 17 简化版实现
let isBatchingUpdates = false;
let updateQueue = [];
function batchedUpdates(callback) {
const alreadyBatchingUpdates = isBatchingUpdates;
isBatchingUpdates = true;
try {
return callback();
} finally {
isBatchingUpdates = alreadyBatchingUpdates;
if (!isBatchingUpdates) {
flushBatchedUpdates();
}
}
}
function flushBatchedUpdates() {
while (updateQueue.length) {
const update = updateQueue.shift();
update.perform();
}
}
关键点:
isBatchingUpdates标志位决定是否正在批量更新batchedUpdates函数开启批量模式,执行回调后刷新队列- 事件处理器中 React 自动启用批量更新
2.2 局限性
在 React 17 及更早版本中,批量更新主要限于事件处理器:
// React 17: setTimeout 中的更新不会批量处理
function handleClick() {
setTimeout(() => {
setCount1(c => c + 1); // 触发一次渲染
setCount2(c => c + 1); // 再触发一次渲染
}, 1000);
}
注意: 异步回调中的状态更新不会被批量处理,每个
setState会触发单独的渲染。
3. React 18 的自动批量更新
3.1 核心改进
React 18 引入了自动批量更新,即使在异步代码中也能合并状态更新:
import React, { useState } from 'react';
import ReactDOM from 'react-dom/client';
function Counter() {
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
function handleAsyncUpdate() {
setTimeout(() => {
setCount1(c => c + 1); // 自动合并
setCount2(c => c + 1); // 自动合并
// 只会触发一次渲染!
}, 1000);
}
return (
<div>
<p>Count 1: {count1}</p>
<p>Count 2: {count2}</p>
<button onClick={handleAsyncUpdate}>异步更新</button>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);
3.2 性能收益
| 场景 | React 17 | React 18 |
|---|---|---|
| 事件处理器中的多次更新 | 1 次渲染 | 1 次渲染 |
| setTimeout 中的多次更新 | 多次渲染 | 1 次渲染 |
| Promise.then 中的多次更新 | 多次渲染 | 1 次渲染 |
| fetch 回调中的多次更新 | 多次渲染 | 1 次渲染 |
3.3 手动批量更新
如果需要在特定场景手动批量更新,可以使用 unstable_batchedUpdates:
import { unstable_batchedUpdates } from 'react-dom';
function handleClick() {
unstable_batchedUpdates(() => {
setCount1(c => c + 1);
setCount2(c => c + 1);
});
}
提示: React 18 中大多数场景无需手动调用,框架会自动处理。
4. 批量更新流程图
graph TD
A[状态更新触发] --> B{是否在批量上下文中?}
B -->|是| C[加入更新队列]
B -->|否| D[立即处理更新]
C --> E{批量操作结束?}
E -->|否| F[等待更多更新]
E -->|是| G[执行批量渲染]
D --> H[执行单次渲染]
G --> I[组件重新渲染]
H --> I
5. 与其他 React 18 特性结合
5.1 Transition API
批量更新与 startTransition 结合,可以区分紧急和非紧急更新:
import { useTransition, useState } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
function handleChange(e) {
const value = e.target.value;
setQuery(value); // 紧急更新 - 输入框立即响应
startTransition(() => {
// 非紧急更新 - 搜索结果批量处理
searchAPI(value);
});
}
}
5.2 并发渲染
React 18 的并发渲染允许中断和恢复渲染,这是 React 的一次重大架构升级。
5.2.1 什么是并发渲染
传统 React(17 及之前)的渲染是同步的:一旦开始渲染,就会阻塞主线程,直到渲染完成。如果组件树很大,用户交互可能会被延迟,导致卡顿。
并发渲染则不同:React 可以同时准备多个版本的 UI,根据用户交互的优先级动态切换。
graph TD
A[用户输入] --> B{紧急更新?}
B -->|是| C[高优先级渲染]
B -->|否| D[低优先级渲染]
C --> E[立即响应]
D --> F[可中断]
F --> G[更紧急的更新到达]
G --> C
F --> H[完成渲染]
5.2.2 核心概念:Fiber 架构
React 18 的并发渲染基于 Fiber 架构。Fiber 是 React 内部的一种数据结构,将渲染工作拆分成小单元:
// 传统渲染:一次性完成
// 组件A → 组件B → 组件C → DOM 更新
// Fiber 渲染:分片完成
// [工作单元1] → [工作单元2] → [工作单元3] → ... → DOM 更新
// ↑ ↓
// 中断 恢复
5.2.3 useDeferredValue
useDeferredValue 用于延迟非关键内容的渲染:
import { useDeferredValue, useState, useMemo } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// 大量数据的筛选 - 使用延迟值
const filteredItems = useMemo(
() => items.filter(item =>
item.name.toLowerCase().includes(deferredQuery.toLowerCase())
),
[deferredQuery]
);
// 样式区分:延迟渲染时显示不同状态
const isStale = query !== deferredQuery;
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="搜索..."
/>
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{filteredItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
);
}
5.2.4 useTransition
startTransition 标记非紧急更新:
import { useTransition, useState } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [activeTab, setActiveTab] = useState('posts');
function handleTabChange(tab) {
// 立即更新 UI 状态
setActiveTab(tab);
// 但内容切换可以延迟
startTransition(() => {
// 这个更新会被标记为低优先级
fetchTabData(tab);
});
}
return (
<div>
<button onClick={() => handleTabChange('posts')}>文章</button>
<button onClick={() => handleTabChange('comments')}>评论</button>
<button onClick={() => handleTabChange('settings')}>设置</button>
{isPending && <LoadingSpinner />}
<TabContent activeTab={activeTab} />
</div>
);
}
5.2.5 并发渲染的工作流程
sequenceDiagram
participant User as 用户
participant React as React 引擎
participant Fiber as Fiber 调度器
participant DOM as DOM
User->>React: 输入搜索关键词
React->>Fiber: 创建高优先级任务
Fiber->>Fiber: 中断低优先级渲染
Fiber->>DOM: 优先更新输入框
Note over Fiber: 用户看到即时反馈
Fiber->>DOM: 完成搜索结果渲染
Note over DOM: 显示搜索结果
5.2.6 使用场景对比
| 场景 | 解决方案 | 说明 |
|---|---|---|
| 搜索输入 + 结果列表 | useDeferredValue | 输入即时响应,结果延迟渲染 |
| 标签页切换 | useTransition | 切换按钮立即响应,内容可延迟 |
| 大列表滚动 | useDeferredValue | 滚动时保持流畅 |
| 表单验证 | 无需处理 | 验证通常是紧急的 |
5.2.7 注意事项
// ❌ 错误:不要对状态值使用 useDeferredValue
function BadExample() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
// 这没有意义,query 和 deferredQuery 本质相同
return <div>{deferredQuery}</div>;
}
// ✅ 正确:deferredQuery 用于派生计算
function GoodExample() {
const [query, setQuery] = useState('');
const deferredQuery = useDeferredValue(query);
const filteredItems = useMemo(
() => expensiveFilter(allItems, deferredQuery),
[deferredQuery]
);
return <List items={filteredItems} />;
}
5.2.8 与批量更新的关系
并发渲染和批量更新是互补的:
function App() {
const [count, setCount] = useState(0);
const [isPending, startTransition] = useTransition();
function handleClick() {
setCount(c => c + 1); // 高优先级更新 - 自动批量
startTransition(() => {
// 低优先级更新 - 可中断
setFilterValue(newValue);
setSearchResults(newResults);
});
}
}
- 批量更新: 合并多个
setState为一次渲染 - 并发渲染: 根据优先级决定渲染顺序和是否中断
两者结合,React 18 能够智能地处理各种更新场景,提供流畅的用户体验。
6. 总结
React 批量更新机制经历了重要演进:
- React 17: 仅在事件处理器中自动批量更新
- React 18: 自动批量更新扩展到所有场景
这项改进带来了显著的性能提升,开发者无需担心状态更新的上下文,React 会智能地合并渲染。