flushSync
flushSync 强制 React 同步执行状态更新,而不是批处理或延迟。
核心概述
⚠️ 痛点: React 异步批处理导致状态不同步
- • React 自动批处理多个状态更新以提高性能
- • 状态更新是异步的,无法立即获取最新值
- • DOM 操作需要最新状态但 React 还未更新 DOM
- • 第三方库集成需要同步更新(如 D3、Chart.js)
✅ 解决方案: flushSync 强制同步更新
- • 立即执行更新: 强制 React 同步渲染
- • 获取最新 DOM: 确保 DOM 已更新
- • 打破批处理: 不等待其他更新
- • 慎用: 仅在必要时使用,会影响性能
💡 心智模型: 紧急通道
将 flushSync 想象成"紧急通道":
- • 正常流程: React 批处理优化(类似普通通道)
- • 紧急情况: 需要立即更新时使用 flushSync
- • 优先通过: 跳过批处理队列,立即执行
- • 成本较高: 频繁使用会影响性能
技术规格
类型签名
import { flushSync } from 'react-dom';
function flushSync<R>(fn: () => R): R参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
fn | () => R | 包含状态更新的回调函数 |
⚠️ 注意: flushSync 仅在 ReactDOM 的 concurrent 模式下有效。如果你使用的是 React 18 的 createRoot,默认启用 concurrent 模式。
实战演练
示例 1: 基础用法
import { flushSync } from 'react-dom';
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ❌ 普通更新 - 异步批处理
setCount(count + 1);
console.log(count); // 仍然是旧值: 0
// ✅ 使用 flushSync - 同步更新
flushSync(() => {
setCount(c => c + 1);
});
console.log(count); // 新值: 1
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>增加</button>
</div>
);
}示例 2: DOM 测量
import { flushSync } from 'react-dom';
import { useState, useRef } from 'react';
function MeasureExample() {
const [width, setWidth] = useState(0);
const divRef = useRef<HTMLDivElement>(null);
const expandAndMeasure = () => {
// ❌ 错误: DOM 还未更新
setWidth(300);
console.log(divRef.current?.offsetWidth); // 旧值
// ✅ 正确: 使用 flushSync
flushSync(() => {
setWidth(300);
});
// 现在 DOM 已更新
console.log(divRef.current?.offsetWidth); // 新值: 300
};
return (
<div>
<div
ref={divRef}
style={{ width: `${width}px`, transition: 'width 0.3s' }}
>
内容区域
</div>
<button onClick={expandAndMeasure}>
展开并测量
</button>
</div>
);
}示例 3: 第三方库集成(D3.js)
import { flushSync } from 'react-dom';
import { useState, useEffect, useRef } from 'react';
import * as d3 from 'd3';
function D3Chart({ data }: { data: number[] }) {
const svgRef = useRef<SVGSVGElement>(null);
useEffect(() => {
// ✅ 确保 DOM 更新后再绘制 D3 图表
flushSync(() => {
// 这里不需要,因为我们已经在 useEffect 中
// 但在某些情况下,可能需要强制同步
});
// 绘制 D3 图表
const svg = d3.select(svgRef.current);
svg
.selectAll('circle')
.data(data)
.enter()
.append('circle')
.attr('cx', (d, i) => i * 50)
.attr('cy', d => d)
.attr('r', 10);
}, [data]);
return <svg ref={svgRef} width={500} height={300} />;
}
// 使用场景:
// - D3.js 图表
// - Chart.js 图表
// - 测量 DOM 尺寸
// - 滚动到新元素
// - 与第三方动画库集成避坑指南
❌ 错误 1: 滥用 flushSync
// ❌ 错误: 不必要地使用 flushSync
function Form() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
flushSync(() => {
// 每次输入都强制同步更新
setName(e.target.value);
});
};
return <input onChange={handleChange} />;
}
// 问题:
// 1. 失去批处理优化
// 2. 性能严重下降
// 3. 输入卡顿
// 4. 不必要的同步渲染✅ 正确 1: 只在必要时使用
// ✅ 正确: 大多数情况使用普通更新
function Form() {
const [name, setName] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// 普通更新 - React 自动批处理
setName(e.target.value);
};
return <input onChange={handleChange} />;
}
// ✅ 正确: 只在需要立即 DOM 时使用 flushSync
function ScrollToElement() {
const [items, setItems] = useState([1, 2, 3]);
const addItemAndScroll = () => {
const newItem = items.length + 1;
// 需要立即获取新添加元素的 DOM
flushSync(() => {
setItems([...items, newItem]);
});
// 现在 DOM 已更新,可以滚动
const element = document.getElementById(`item-${newItem}`);
element?.scrollIntoView({ behavior: 'smooth' });
};
return (
<div>
<button onClick={addItemAndScroll}>添加并滚动</button>
{items.map(item => (
<div key={item} id={`item-${item}`}>
Item {item}
</div>
))}
</div>
);
}❌ 错误 2: 在 flushSync 中执行副作用
// ❌ 错误: 在 flushSync 中有副作用
function Component() {
const [data, setData] = useState(null);
const handleClick = () => {
flushSync(() => {
setData(newData);
// 副作用: API 调用
fetch('/api/log').then(() => {
console.log('logged');
});
});
};
return <button onClick={handleClick}>更新</button>;
}
// 问题:
// 1. 副作用执行时机不确定
// 2. 可能导致重复调用
// 3. 违反 React 设计原则✅ 正确 2: 在 flushSync 后执行副作用
// ✅ 正确: 先更新,再执行副作用
function Component() {
const [data, setData] = useState(null);
const handleClick = () => {
// 1. 强制同步更新状态
flushSync(() => {
setData(newData);
});
// 2. DOM 更新后执行副作用
fetch('/api/log').then(() => {
console.log('logged');
});
};
return <button onClick={handleClick}>更新</button>;
}
// 或者使用 useEffect
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
if (data) {
// 数据更新后执行副作用
fetch('/api/log', {
method: 'POST',
body: JSON.stringify(data)
});
}
}, [data]);
return <button onClick={() => setData(newData)}>更新</button>;
}❌ 错误 3: 嵌套使用 flushSync
// ❌ 错误: 嵌套 flushSync
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const handleClick = () => {
flushSync(() => {
setA(1);
flushSync(() => {
// 嵌套 flushSync - 可能导致问题
setB(2);
});
});
};
return <button onClick={handleClick}>更新</button>;
}
// 问题:
// 1. React 会发出警告
// 2. 可能导致死循环
// 3. 性能严重下降✅ 正确 3: 批量更新后使用一次 flushSync
// ✅ 正确: 批量更新
function Component() {
const [a, setA] = useState(0);
const [b, setB] = useState(0);
const handleClick = () => {
flushSync(() => {
setA(1);
setB(2);
// 所有更新在一次 flushSync 中
});
};
return <button onClick={handleClick}>更新</button>;
}
// 优点:
// 1. 只强制同步一次
// 2. 性能更好
// 3. React 不会警告最佳实践
1. 使用场景检查清单
// 使用 flushSync 前先检查:
// ✅ 真的需要立即获取 DOM 吗?
// ✅ 是否可以重构为异步?
// ✅ 是否可以用 useEffect 替代?
// ✅ 是否可以用 ref 替代?
// 常见合理使用场景:
// 1. DOM 测量
function measure() {
flushSync(() => setState(1));
const rect = element.getBoundingClientRect();
}
// 2. 滚动到新元素
function scroll() {
flushSync(() => addItem(newItem));
document.getElementById(newItem)?.scrollIntoView();
}
// 3. 第三方库集成(D3, Chart.js)
function drawChart() {
flushSync(() => setData(chartData));
d3.select(svg).selectAll('circle').data(data);
}
// 4. 同步打印/export
function print() {
flushSync(() => setContent(printContent));
window.print();
}2. 性能考虑
// 性能对比
// ❌ 差: 频繁使用 flushSync
function BadPerformance() {
const [value, setValue] = useState(0);
const handleChange = (e) => {
// 每次输入都强制同步 - 非常慢!
flushSync(() => {
setValue(e.target.value);
});
};
return <input value={value} onChange={handleChange} />;
}
// ✅ 好: 使用普通更新 + useEffect
function GoodPerformance() {
const [value, setValue] = useState(0);
const handleChange = (e) => {
// 普通更新 - React 批处理优化
setValue(e.target.value);
};
useEffect(() => {
// DOM 更新后执行操作
console.log('DOM updated:', value);
}, [value]);
return <input value={value} onChange={handleChange} />;
}
// 性能差异:
// - 普通: 批处理,每秒渲染 1-2 次
// - flushSync: 每次输入都渲染,每秒渲染数十次3. 替代方案
| 需求 | 不推荐 | 推荐 |
|---|---|---|
| DOM 更新后执行操作 | flushSync | useEffect + ref |
| 获取最新状态 | flushSync | setState(prev => prev + 1) |
| DOM 测量 | flushSync | useLayoutEffect |
| 第三方库集成 | flushSync | useEffect + ref |
相关链接
- useLayoutEffect Hook - 同步执行副作用
- useEffect Hook - 异步执行副作用
- 渲染和提交 - 理解 React 渲染流程