React18的自动批处理与flushSync的使用场景
React 18 引入了自动批处理机制,这是对性能优化的一次重大升级。同时,为了应对特殊场景,新增了 flushSync API 允许开发者手动退出批处理。
理解批处理的核心逻辑
批处理是指 React 将多个状态更新合并到一次重新渲染中,以提高性能。
在 React 18 之前,批处理仅在浏览器事件(如点击)中生效。如果在 Promise、setTimeout 或原生 DOM 事件中更新状态,React 不会进行批处理,导致组件渲染多次。
React 18 的自动批处理打破了这一限制,无论更新发生在何处,React 都会自动合并更新。
自动批处理的实际表现
通过代码对比可以直观理解 React 18 的变化。
场景一:Promise 或 setTimeout 内部更新
假设有一个组件,点击按钮后在 setTimeout 中连续更新两个状态。
-
创建 一个函数组件,包含两个独立的状态
count和flag。import { useState } from 'react'; function MyComponent() { const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); function handleClick() { setTimeout(() => { // React 17: 渲染两次 (count变,flag变) // React 18: 渲染一次 (自动批处理) setCount(c => c + 1); setFlag(f => !f); }, 0); } return ( <button onClick={handleClick}> Count: {count}, Flag: {String(flag)} </button> ); } -
观察 渲染行为。
- 在 React 17 中,
setCount和setFlag各触发一次渲染。 - 在 React 18 中,React 检测到这两次更新发生在同一个事件循环上下文中,自动合并 为一次渲染。
- 在 React 17 中,
场景二:跨组件更新
当多个组件的状态在同一个回调中更新时,自动批处理同样生效。
-
定义 两个子组件和一个父组件。
function ChildA({ onClick }) { return <button onClick={onClick}>Click Me</button>; } function ChildB({ count }) { return <p>Count: {count}</p>; } export default function App() { const [count, setCount] = useState(0); const [text, setText] = useState(''); const handleClick = () => { fetch('/api/data').then(() => { // 异步回调中的自动批处理 setCount(1); setText('Updated'); // React 18 只会重新渲染一次 }); }; return ( <> <ChildA onClick={handleClick} /> <ChildB count={count} /> </> ); }
flushSync 的使用场景
虽然自动批处理对性能有利,但在某些极端场景下,我们需要立即获取更新后的 DOM 状态,此时必须禁用批处理。flushSync 就是为了解决这个问题。
核心作用
flushSync 会强制 React 同步执行回调函数内的所有更新,并立即刷新 DOM,确保后续代码能够读取到最新的 DOM 状态。
使用步骤
-
导入
flushSync从react-dom。import { flushSync } from 'react-dom'; -
包裹 需要立即更新的状态逻辑。
function handleChange() { // 强制同步更新 flushSync(() => { setCount(count + 1); }); // 代码执行到这里时,DOM 已经完成更新 console.log(document.getElementById('my-div').textContent); }
典型案例:聚焦新输入框
假设点击按钮后新增一个输入框,并需要立即让该输入框获得焦点。如果没有 flushSync,React 可能会在事件处理结束后才批量渲染,导致 focus 代码执行时输入框还不存在。
-
编写 一个添加输入框的逻辑。
import { useState, useRef } from 'react'; import { flushSync } from 'react-dom'; function FormList() { const [inputs, setInputs] = useState([{ id: 1 }]); const newInputRef = useRef(null); function handleAdd() { const newId = inputs.length + 1; // 错误做法(不使用 flushSync): // setInputs([...inputs, { id: newId }]); // newInputRef.current?.focus(); // 此时 DOM 尚未更新,current 为 null // 正确做法: flushSync(() => { setInputs([...inputs, { id: newId }]); }); // 此时 DOM 已经包含新输入框 // 注意:这里通常需要配合 ref 回调或 useEffect,但在 flushSync 后可直接操作 DOM document.getElementById(`input-${newId}`)?.focus(); } return ( <div> {inputs.map(input => ( <input key={input.id} id={`input-${input.id}`} /> ))} <button onClick={handleAdd}>Add Input</button> </div> ); }
自动批处理与 flushSync 的决策流程
为了清晰判断何时使用默认行为,何时使用 flushSync,可以参考以下逻辑:
关键注意事项
使用 flushSync 时必须极其谨慎,因为它会显著降低性能。
- 避免 在
flushSync回调外部进行不必要的逻辑包裹。 - 确认 只有在确实需要同步读取 DOM 更新结果时才使用。
- 警惕
flushSync会刷新整个组件树的挂起状态,而不仅仅是包裹的组件。
| 特性 | 自动批处理 (默认) | flushSync (强制同步) |
|---|---|---|
| 更新方式 | 异步、合并 | 同步、立即 |
| 渲染次数 | 多次更新合并为 1 次 | 每次更新立即渲染 |
| DOM 状态 | 更新后无法立即读取 | 更新后立即可用 |
| 性能影响 | 高 (优) | 低 (可能卡顿) |
| 适用场景 | 绝大多数业务逻辑 | 焦点管理、滚动定位、第三方库集成 |

暂无评论,快来抢沙发吧!