跳到主要内容

setState 批量更新

问题

setState 是同步还是异步的?什么是批量更新?React 18 有什么变化?

答案

setState 本身是同步执行的,但状态的更新是批量的。React 会将多次 setState 合并为一次渲染,这就是批量更新(Batching)。React 18 引入了自动批处理,在任何情况下都会进行批量更新。

setState 的"异步"表现

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
setCount(count + 1);
console.log(count); // 输出 0,不是 1

setCount(count + 1);
console.log(count); // 还是 0
};

return <button onClick={handleClick}>{count}</button>;
}
为什么看起来是"异步"的?

setState 调用后,count 没有立即变化,是因为:

  1. count 是当前渲染闭包中的值
  2. 新的值要在下次渲染时才能获取
  3. React 会批量处理多个 setState

这不是真正的异步(不涉及 Promise/setTimeout),而是 React 的批量更新机制

批量更新(Batching)

什么是批量更新?

React 会将同一事件循环中的多个 setState 合并,只触发一次重新渲染。

function Example() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
const [name, setName] = useState('');

const handleClick = () => {
setCount(1);
setFlag(true);
setName('Alice');
// 三次 setState,只触发一次重新渲染
};
}

React 17 的批量更新限制

在 React 17 中,批量更新只在 React 事件处理器中生效

// React 17
function Example() {
const [count, setCount] = useState(0);

// ✅ React 事件处理器:批量更新
const handleClick = () => {
setCount(c => c + 1);
setCount(c => c + 1);
// 只渲染一次
};

// ❌ setTimeout:不批量更新
const handleTimeout = () => {
setTimeout(() => {
setCount(c => c + 1); // 渲染一次
setCount(c => c + 1); // 再渲染一次
}, 0);
};

// ❌ Promise:不批量更新
const handleFetch = async () => {
await fetch('/api');
setCount(c => c + 1); // 渲染一次
setCount(c => c + 1); // 再渲染一次
};

// ❌ 原生事件:不批量更新
useEffect(() => {
const button = document.getElementById('btn');
button?.addEventListener('click', () => {
setCount(c => c + 1); // 渲染一次
setCount(c => c + 1); // 再渲染一次
});
}, []);
}

React 18 自动批处理

React 18 引入了自动批处理(Automatic Batching),在任何情况下都会进行批量更新:

// React 18
function Example() {
const [count, setCount] = useState(0);

// ✅ setTimeout:自动批量更新
const handleTimeout = () => {
setTimeout(() => {
setCount(c => c + 1);
setCount(c => c + 1);
// 只渲染一次!
}, 0);
};

// ✅ Promise:自动批量更新
const handleFetch = async () => {
await fetch('/api');
setCount(c => c + 1);
setCount(c => c + 1);
// 只渲染一次!
};

// ✅ 原生事件:自动批量更新
useEffect(() => {
const button = document.getElementById('btn');
button?.addEventListener('click', () => {
setCount(c => c + 1);
setCount(c => c + 1);
// 只渲染一次!
});
}, []);
}
场景React 17React 18
React 事件处理器✅ 批量✅ 批量
setTimeout/setInterval❌ 不批量批量
Promise.then❌ 不批量批量
原生事件❌ 不批量批量
fetch/XHR 回调❌ 不批量批量

退出批量更新:flushSync

如果确实需要立即更新 DOM,可以使用 flushSync

import { flushSync } from 'react-dom';

function Example() {
const [count, setCount] = useState(0);

const handleClick = () => {
flushSync(() => {
setCount(c => c + 1);
});
// DOM 已更新
console.log(document.getElementById('count')?.textContent); // '1'

flushSync(() => {
setCount(c => c + 1);
});
// DOM 已更新
console.log(document.getElementById('count')?.textContent); // '2'
};

return <div id="count">{count}</div>;
}
注意

flushSync强制同步渲染,可能影响性能。只在确实需要时使用,如:

  • 需要立即读取更新后的 DOM
  • 与第三方库集成

函数式更新 vs 直接更新

直接更新的问题

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
// 直接更新:基于当前闭包中的 count
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1(还是基于 0)
setCount(count + 1); // 0 + 1 = 1
// 最终 count = 1
};
}

函数式更新

function Counter() {
const [count, setCount] = useState(0);

const handleClick = () => {
// 函数式更新:基于最新的 state
setCount(prev => prev + 1); // 0 + 1 = 1
setCount(prev => prev + 1); // 1 + 1 = 2
setCount(prev => prev + 1); // 2 + 1 = 3
// 最终 count = 3
};
}

何时使用函数式更新?

场景推荐方式原因
新值不依赖旧值直接更新简单直观
新值依赖旧值函数式更新确保获取最新值
多次连续更新函数式更新避免覆盖
异步回调中更新函数式更新避免闭包陷阱
// ✅ 新值不依赖旧值
setName('Alice');
setVisible(true);

// ✅ 新值依赖旧值
setCount(prev => prev + 1);
setItems(prev => [...prev, newItem]);

// ✅ 异步回调
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 正确
// setCount(count + 1); // 错误:count 永远是 0
}, 1000);
return () => clearInterval(timer);
}, []);

类组件的 setState

类组件批量更新

class Counter extends React.Component {
state = { count: 0 };

handleClick = () => {
// 同样会批量更新
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 最终 count = 1(不是 3)
};

handleClickCorrect = () => {
// 使用函数式更新
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
this.setState(prev => ({ count: prev.count + 1 }));
// 最终 count = 3
};
}

setState 的第二个参数

类组件的 setState 支持回调函数:

class Counter extends React.Component {
state = { count: 0 };

handleClick = () => {
this.setState(
{ count: this.state.count + 1 },
() => {
// 回调在 state 更新后、DOM 更新后执行
console.log(this.state.count); // 1
}
);
};
}
函数组件替代方案

函数组件没有 setState 回调,可以使用 useEffect

function Counter() {
const [count, setCount] = useState(0);

useEffect(() => {
console.log(count); // 在 count 变化后执行
}, [count]);
}

批量更新原理

React 17 原理

React 17 使用执行上下文判断是否批量更新:

// 简化的批量更新原理
let isBatchingUpdates = false;
const updateQueue: Array<() => void> = [];

function setState(update: any): void {
updateQueue.push(update);

if (!isBatchingUpdates) {
// 不在批量更新中,立即执行
flushUpdates();
}
// 否则等待批量执行
}

function batchedUpdates(fn: () => void): void {
isBatchingUpdates = true;
fn();
isBatchingUpdates = false;
flushUpdates(); // 批量执行所有更新
}

// React 事件处理器会被包裹
function handleClick() {
batchedUpdates(() => {
// 事件处理器代码
});
}

React 18 原理

React 18 使用调度器优先级实现自动批处理:

// React 18 简化原理
function setState(update: any): void {
// 将更新加入队列
enqueueUpdate(update);

// 调度一次渲染(而不是立即渲染)
scheduleUpdateOnFiber();
}

function scheduleUpdateOnFiber(): void {
// 确保只有一个渲染被调度
if (!isScheduled) {
isScheduled = true;
// 使用微任务调度
queueMicrotask(() => {
isScheduled = false;
performConcurrentWorkOnRoot();
});
}
}

常见面试问题

Q1: setState 是同步还是异步的?

答案

setState 本身是同步调用的,但更新是批量异步的。

const [count, setCount] = useState(0);

const handleClick = () => {
setCount(1); // 同步调用
console.log(count); // 仍然是 0
};

原因

  1. React 会将多个 setState 批量合并,只触发一次渲染
  2. count 是当前渲染闭包中的值,不会在本次渲染中改变
  3. 新值在下次渲染时才能获取

Q2: React 17 和 React 18 的批量更新有什么区别?

答案

特性React 17React 18
React 事件✅ 批量✅ 批量
setTimeout❌ 不批量批量
Promise❌ 不批量批量
原生事件❌ 不批量批量

React 18 的自动批处理让所有场景都能享受批量更新的性能优化。

// React 18
setTimeout(() => {
setCount(1);
setFlag(true);
// 只渲染一次
}, 0);

Q3: 如何在 setState 后立即获取最新的 state?

答案

方案 1:使用 useEffect

const [count, setCount] = useState(0);

useEffect(() => {
console.log('count changed:', count);
}, [count]);

const handleClick = () => {
setCount(prev => prev + 1);
};

方案 2:使用 flushSync

import { flushSync } from 'react-dom';

const handleClick = () => {
flushSync(() => {
setCount(prev => prev + 1);
});
// DOM 已更新,但 count 变量仍是旧值(闭包)
console.log(document.getElementById('counter')?.textContent);
};

方案 3:使用 useRef 存储

const countRef = useRef(count);

useEffect(() => {
countRef.current = count;
}, [count]);

const handleClick = () => {
setCount(prev => prev + 1);
// 下一个事件循环中可以读取
setTimeout(() => {
console.log(countRef.current);
}, 0);
};

Q4: 函数式更新和直接更新有什么区别?

答案

方式示例特点
直接更新setCount(count + 1)基于闭包中的值
函数式更新setCount(prev => prev + 1)基于最新的 state
// 直接更新:三次都基于 count=0
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1
setCount(count + 1); // 0 + 1 = 1
// 最终 count = 1

// 函数式更新:依次计算
setCount(prev => prev + 1); // 0 + 1 = 1
setCount(prev => prev + 1); // 1 + 1 = 2
setCount(prev => prev + 1); // 2 + 1 = 3
// 最终 count = 3

建议:当新值依赖旧值时,始终使用函数式更新。

Q5: 什么是 flushSync?什么时候使用?

答案

flushSync强制同步执行更新,跳过批量处理。

import { flushSync } from 'react-dom';

flushSync(() => {
setCount(1);
});
// 此时 DOM 已更新

使用场景

  • 需要立即读取更新后的 DOM
  • 第三方库集成(如动画库需要读取 DOM)
  • 某些测试场景

注意

  • 会影响性能,应尽量避免
  • 不会改变闭包中的变量值

相关链接