在 React 17 及之前版本中,setState 是否同步更新,取决于它被调用的上下文环境。
一、同步更新的情况
1. 在原生事件或 setTimeout/setInterval、Promise 等“异步”环境下
结论:
在这些环境下,setState 是同步的,也就是说,状态会立即更新,后续代码能拿到最新的 state。
示例1:setTimeout 中 setState
import React, { useState, useEffect } from 'react';
function TimeoutSyncExample() {
const [count, setCount] = useState(0);
useEffect(() => {
setTimeout(() => {
setCount(count + 1);
console.log('setTimeout后,count:', count); // 这里count是旧值
}, 1000);
}, [count]);
console.log('render了一次,count:', count);
return <div>{count}</div>;
}
注意:
虽然 setState 在 setTimeout 中是同步的,但由于闭包原因,console.log 里拿到的 count 还是旧值。
如果你在 setTimeout 里连续多次 setState,React 17 及之前会同步执行多次,每次都会触发一次 render。
示例2:原生事件中 setState
import React, { useState, useRef, useEffect } from 'react';
function NativeEventSyncExample() {
const [count, setCount] = useState(0);
const btnRef = useRef(null);
useEffect(() => {
const handler = () => {
setCount(count + 1);
console.log('原生事件后,count:', count); // 这里count是旧值
};
btnRef.current.addEventListener('click', handler);
return () => btnRef.current.removeEventListener('click', handler);
}, [count]);
return <button ref={btnRef}>点击</button>;
}
说明:
在原生事件(非 React 合成事件)中,setState 也是同步的。
二、异步批量更新的情况
1. React 合成事件、生命周期函数、useEffect 等
结论:
在 React 的合成事件(如 onClick)、生命周期函数(如 componentDidMount)、useEffect 等环境下,setState 是异步批量更新的。
也就是说,setState 不会立刻更新 state,只有等事件处理函数执行完毕后,React 才会统一批量更新 state 并重新渲染组件。
示例3:合成事件中 setState
import React, { useState } from 'react';
function SyntheticEventAsyncExample() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
console.log('合成事件后,count:', count); // 这里count是旧值
setCount(count + 1);
console.log('合成事件后,count:', count); // 这里count还是旧值
};
return <button onClick={handleClick}>点击</button>;
}
说明:
在合成事件中,setState 是异步的,console.log 拿到的 count 还是旧值。
而且多次 setState 会被合并(批量更新),只会触发一次 render。
三、详细代码讲解
1. setState 在不同环境下的表现
import React, { useState, useEffect, useRef } from 'react';
function SetStateDemo() {
const [count, setCount] = useState(0);
const btnRef = useRef(null);
// React 合成事件
const handleClick = () => {
setCount(count + 1);
console.log('合成事件中,count:', count); // 旧值
setCount(count + 1);
console.log('合成事件中,count:', count); // 旧值
};
// 原生事件
useEffect(() => {
const handler = () => {
setCount(count + 1);
console.log('原生事件中,count:', count); // 旧值
setCount(count + 1);
console.log('原生事件中,count:', count); // 旧值
};
btnRef.current.addEventListener('click', handler);
return () => btnRef.current.removeEventListener('click', handler);
}, [count]);
// setTimeout
useEffect(() => {
setTimeout(() => {
setCount(count + 1);
console.log('setTimeout中,count:', count); // 旧值
setCount(count + 1);
console.log('setTimeout中,count:', count); // 旧值
}, 2000);
}, [count]);
// Promise
useEffect(() => {
Promise.resolve().then(() => {
setCount(count + 1);
console.log('Promise中,count:', count); // 旧值
setCount(count + 1);
console.log('Promise中,count:', count); // 旧值
});
}, [count]);
return (
<div>
<button onClick={handleClick}>React合成事件</button>
<button ref={btnRef}>原生事件</button>
<div>Count: {count}</div>
</div>
);
}
四、总结
- React 17 及之前版本,setState 是否同步,取决于调用环境:
- React 合成事件、生命周期、useEffect 等:异步批量更新
- 原生事件、setTimeout、Promise 等:同步更新
- 多次 setState 行为:
- 合成事件中多次 setState 会被合并,只触发一次 render。
- 异步环境下多次 setState 不会合并,每次都会触发 render。
五、深入理解
为什么会有这种区别?
React 为了性能优化,在合成事件和生命周期中采用了批量更新机制(Batching),这样可以减少不必要的渲染。而在原生事件、setTimeout、Promise 等环境下,React 无法感知这些事件的边界,所以只能同步更新。
六、扩展
-
React 18 开始,
setState在所有环境下都采用了批量更新(即使在 setTimeout、Promise 里也是异步的),这是 React 18 的新特性。 -
你可以通过
ReactDOM.unstable_batchedUpdates手动实现批量更新。