状态更新队列:批处理机制与更新函数 updater
在上一关中,你发现连续写三次 setNumber(number + 1) 最终只增加了 1,因为每次调用的都是当次快照里的同一个数字。
但如果你真的需要在一个点击事件里多次累加状态(比如点击一次完成三道连击计算,或者连续分步修改同一状态),该怎么做呢?
答案是:使用更新函数(Updater Function),并借助 React 底层的状态更新队列(Queueing)。
1. 餐厅服务员的“批处理(Batching)”艺术
在解释更新函数之前,先理解 React 是如何处理状态更新请求的。
想象你坐在餐厅里:
- 你对服务员说:“给我来一份沙拉”;
- 还没等服务员转身,你紧接着说:“再加一份牛排”;
- 紧接着又说:“还有一杯红酒”。
服务员会每次一听你说完一个单词,就立刻风风火火跑回后厨下单一次,一共来回跑三次吗? 当然不会! 服务员会耐心等你把整句话说完,一次性把菜单记在小本本上,然后才走进后厨统一交给厨师。
React 也是这样工作的!在 React 中,直到事件处理函数内部的所有同步代码全部执行完毕,React 才会统一着手进行重新渲染。这一特性被称为 批处理(Batching)。 批处理极大地保护了应用的性能,避免了用户每点一次按钮,整个页面就疯狂抖动重新渲染三四次。
2. 使用更新函数(Updater Function)
如果你希望在下一次渲染之前,基于排队中的前一个更新结果继续计算,你可以向 set 函数传递一个以旧状态为参数、返回新状态的函数,而不是传递一个死的值:
import { useState } from 'react';
export default function Counter() {
const [number, setNumber] = useState(0);
return (
<>
<h1>{number}</h1>
<button onClick={() => {
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
}}>+3</button>
</>
);
}
现在,点击一次按钮,数字如你所愿,稳稳地变成了 3!
3. 深入底层:React 是如何演算更新队列的?
这里传递的 n => n + 1 被称为 更新函数(Updater Function)。
(约定俗成使用该状态变量的首字母命名参数,比如 number 的首字母 n)。
当你写下上面这三行代码时,React 并不是立刻执行计算,而是把这三个函数放进了一个**待处理队列(Queue)**中:
| 队列排入的更新 | 传入的值 / 函数 | 待处理队列中的状态计算结果 |
|---|---|---|
| 第 1 次 | n => n + 1 | 初始值 0 $ |
| ightarrow$ 计算出 1 | ||
| 第 2 次 | n => n + 1 | 带着上一轮的 1 $ |
| ightarrow$ 计算出 2 | ||
| 第 3 次 | n => n + 1 | 带着上一轮的 2 $ |
| ightarrow$ 计算出 3 |
当点击事件函数的所有代码执行完毕后,React 会在后台清空这个队列,把最终算出来的 3 赋给下次渲染的 number,并启动一次界面重绘!
4. 烧脑推演:混合使用值与更新函数
让我们来看一个非常经典的进阶推演题。看看你能否推导出最终屏幕上会显示几?
<button onClick={() => {
setNumber(number + 5);
setNumber(n => n + 1);
setNumber(42);
}}>
假设起始值 number 是 0。点击后:
setNumber(number + 5): 当次快照中number是 0,所以这行等于setNumber(0 + 5),React 把直接值5放入队列;setNumber(n => n + 1): React 发现这是一个更新函数。它读取队列中上一项的结果(5),带入计算得出6并压入队列;setNumber(42): React 发现这是一个直接值,它才不管之前算到了 6,直接把结果强制覆盖替换为42!
事件结束,队列演算完成,最终界面显示的数字是:42!
总结
- React 采用批处理(Batching),等事件函数的所有同步代码执行完后才进行一次统一重新渲染;
- 当你需要基于前序排队状态连续多次累加更新时,传递更新函数
setState(prev => prev + 1); - 更新函数必须是纯函数,不能产生外部副作用;
- 深刻理解队列演算,能让你在处理复杂、高频的状态流转时做到心明眼亮。