setState 同步异步问题
React 中 setState 在不同场景下的执行时机和批处理机制
问题
setState 是同步还是异步?在合成事件、原生事件、setTimeout 中表现有何不同?
解答
结论
setState 本身是同步执行的,但 React 会对状态更新进行批处理(Batching),导致看起来像是”异步”的。
- React 17 及之前:只在合成事件和生命周期中批处理
- React 18:所有场景都自动批处理
React 17 的行为
import React, { Component } from 'react';
class Counter extends Component {
state = { count: 0 };
// 合成事件:批处理,"异步"表现
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 0,还没更新
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 0,还没更新
// 最终 count 为 1,两次 setState 被合并
};
// 原生事件:不批处理,同步更新
bindNativeEvent = () => {
document.getElementById('btn').addEventListener('click', () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 1,立即更新
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 2,立即更新
});
};
// setTimeout:不批处理,同步更新
handleTimeout = () => {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 1,立即更新
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 2,立即更新
}, 0);
};
render() {
return <button onClick={this.handleClick}>{this.state.count}</button>;
}
}
React 18 的自动批处理
import { useState } from 'react';
import { flushSync } from 'react-dom';
function Counter() {
const [count, setCount] = useState(0);
// React 18:所有场景都批处理
const handleClick = () => {
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
// 两次更新被合并,只触发一次渲染
// 最终 count 为 2
}, 0);
};
// 使用 flushSync 强制同步更新
const handleFlushSync = () => {
flushSync(() => {
setCount(c => c + 1);
});
// 此时 DOM 已更新
console.log(document.getElementById('count').textContent); // 更新后的值
flushSync(() => {
setCount(c => c + 1);
});
// 触发两次渲染
};
return <div id="count">{count}</div>;
}
获取更新后的值
// Class 组件:使用回调
this.setState({ count: 1 }, () => {
console.log(this.state.count); // 1
});
// 函数组件:使用 useEffect
useEffect(() => {
console.log(count); // 更新后的值
}, [count]);
批处理原理简述
// 简化的批处理机制
let isBatchingUpdates = false;
let pendingUpdates = [];
function setState(update) {
pendingUpdates.push(update);
if (!isBatchingUpdates) {
// 不在批处理中,立即执行
flushUpdates();
}
// 在批处理中,等待统一执行
}
function batchedUpdates(fn) {
isBatchingUpdates = true;
fn(); // 执行期间的 setState 都会被收集
isBatchingUpdates = false;
flushUpdates(); // 统一处理所有更新
}
关键点
setState本身是同步的,批处理让它表现得像异步- React 17:合成事件和生命周期中批处理,原生事件和 setTimeout 中同步
- React 18:所有场景自动批处理(Automatic Batching)
- 使用
flushSync可以强制同步更新,跳出批处理 - 获取更新后的值:Class 用回调,函数组件用
useEffect
目录