什么是批处理?
批处理是指 React 将多个状态更新合并为单个重新渲染的过程,以提高性能。
React 17 及之前的批处理
TypeScript
// React 17 中,只有在 React 事件处理函数中才会批处理
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// ✅ React 17:在事件处理函数中会被批处理
setCount(c => c + 1);
setFlag(f => !f);
// 只会触发一次重新渲染
}
useEffect(() => {
// ❌ React 17:在 Promise、setTimeout 等中不会批处理
fetchData().then(() => {
setCount(c => c + 1); // 第一次渲染
setFlag(f => !f); // 第二次渲染
});
}, []);
return <button onClick={handleClick}>点击</button>;
}
React 18 的自动批处理
React 18 在任何地方都默认启用自动批处理:
TypeScript
import { useState, useEffect } from 'react';
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
// 1. 事件处理函数 - 仍然批处理
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// ✅ 只触发一次重新渲染
}
// 2. setTimeout - 现在也会批处理
function handleTimeout() {
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// ✅ React 18:只触发一次重新渲染
}, 1000);
}
// 3. Promise - 现在也会批处理
function handlePromise() {
fetch('/api').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// ✅ React 18:只触发一次重新渲染
});
}
// 4. 原生事件处理 - 现在也会批处理
useEffect(() => {
document.getElementById('external-btn').addEventListener('click', () => {
setCount(c => c + 1);
setFlag(f => !f);
// ✅ React 18:只触发一次重新渲染
});
}, []);
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>事件处理</button>
<button onClick={handleTimeout}>setTimeout</button>
<button onClick={handlePromise}>Promise</button>
<button id="external-btn">原生事件</button>
</div>
);
}
如何退出批处理?
在某些情况下,你可能需要立即应用状态更新。React 18 提供了 flushSync
来退出批处理:
TypeScript
import { useState } from 'react';
import { flushSync } from 'react-dom';
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// 立即执行这个更新,不进行批处理
flushSync(() => {
setCount(c => c + 1);
});
// 此时 DOM 已经更新
console.log('Count updated:', count + 1);
// 这个更新会单独处理
setFlag(f => !f);
// 总共会触发两次重新渲染
}
return (
<div>
<p>Count: {count}</p>
<p>Flag: {flag.toString()}</p>
<button onClick={handleClick}>立即更新</button>
</div>
);
}
实际应用场景
1、表单处理
TypeScript
function ContactForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
async function handleSubmit(event) {
event.preventDefault();
setIsSubmitting(true);
try {
// 这些状态更新会被自动批处理
const response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify({ name, email })
});
if (response.ok) {
setName('');
setEmail('');
setIsSubmitting(false);
// ✅ React 18:这三个更新会被批处理为一次渲染
}
} catch (error) {
setIsSubmitting(false);
// 错误处理
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="姓名"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="邮箱"
/>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? '提交中...' : '提交'}
</button>
</form>
);
}
2、购物车操作
TypeScript
function ShoppingCart() {
const [items, setItems] = useState([]);
const [total, setTotal] = useState(0);
const [isLoading, setIsLoading] = useState(false);
async function addItem(product) {
setIsLoading(true);
try {
const response = await fetch('/api/cart/add', {
method: 'POST',
body: JSON.stringify(product)
});
const result = await response.json();
// 这些相关的状态更新会被自动批处理
setItems(prev => [...prev, product]);
setTotal(prev => prev + product.price);
setIsLoading(false);
// ✅ React 18:一次重新渲染而不是三次
} catch (error) {
setIsLoading(false);
console.error('添加商品失败:', error);
}
}
return (
<div>
{/* 组件内容 */}
</div>
);
}
3、与并发特性的结合
TypeScript
import { useState, startTransition } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isSearching, setIsSearching] = useState(false);
function handleSearch(newQuery) {
setQuery(newQuery); // 立即更新输入框
if (newQuery.length === 0) {
// 这些更新会被批处理
setResults([]);
setIsSearching(false);
return;
}
setIsSearching(true);
// 使用 startTransition 标记非紧急更新
startTransition(() => {
searchAPI(newQuery).then(searchResults => {
// 这些更新在并发模式下会被智能批处理
setResults(searchResults);
setIsSearching(false);
});
});
}
return (
<div>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="搜索..."
/>
{isSearching && <div>搜索中...</div>}
<SearchResults results={results} />
</div>
);
}
性能对比
TypeScript
// 性能测试组件
function PerformanceTest() {
const [renderCount, setRenderCount] = useState(0);
const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
const [state3, setState3] = useState(0);
// 记录渲染次数
useEffect(() => {
setRenderCount(prev => prev + 1);
});
function triggerUpdates() {
// 触发多个状态更新
setState1(prev => prev + 1);
setState2(prev => prev + 1);
setState3(prev => prev + 1);
// React 17(非事件处理中):3次重新渲染
// React 18:1次重新渲染
}
return (
<div>
<p>渲染次数: {renderCount}</p>
<p>State1: {state1}</p>
<p>State2: {state2}</p>
<p>State3: {state3}</p>
<button onClick={triggerUpdates}>
触发多个更新
</button>
</div>
);
}
总结
React 18 的自动批处理带来了以下好处:
-
更好的性能:减少不必要的重新渲染
-
更一致的行为:无论在什么情况下,状态更新都有相同的批处理行为
-
更简单的代码:不需要担心批处理的条件,React 会自动处理
-
更好的用户体验:更流畅的界面更新
注意事项:
-
自动批处理是默认开启的,不需要额外配置
-
使用
flushSync
可以退出批处理,但应谨慎使用 -
批处理不会影响逻辑正确性,只是优化了渲染性能
-
与 React 18 的其他新特性(如并发渲染)完美配合
这个特性让 React 应用在各种异步场景下都能获得更好的性能表现。