跳转到正文
Raven
返回

连续调用三次 setState,为什么只加了一次?

在一个点击事件里连续调用三次 setNumber(number + 1),计数器为什么只增加了 1?要解释这个现象,需要同时看清当前渲染的 state 值,以及 React 如何处理更新队列。

先看一个计数器

useState 为组件提供保存状态的变量,以及请求更新状态的 setter。下面的例子从 0 开始,按钮名是“+3”,处理函数也确实调用了三次 setter:

import { useState } from "react";

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button
        onClick={() => {
          setNumber(number + 1);
          setNumber(number + 1);
          setNumber(number + 1);
        }}
      >
        +3
      </button>
    </>
  );
}

按照 React 的更新规则,第一次点击后的值是 1。原因是这三行都读取了同一次渲染中的 number。

每次渲染都有自己的 state 快照

React 渲染函数组件时,会调用组件函数。组件返回的 JSX、事件处理函数和局部变量,都对应这次渲染的 state。

第一次渲染中,number 是 0。调用 setNumber 会请求后续更新,却不会把正在执行的事件处理函数里的 number 改成新值。因此,这三行可以理解成:

setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);

它们分别要求将状态设为 1,没有任何一行读取到前一行提交的新值。下一次渲染拿到的才是新的 state;下一次点击时,事件处理函数对应的 number 就是 1。

这种“快照”含义见 React 官方说明。

要连续累加,就传入更新函数

如果下一次状态依赖上一次状态,原笔记中的函数式写法更合适。将按钮的处理函数替换为:

onClick={() => {
  setNumber(n => n + 1);
  setNumber(n => n + 1);
  setNumber(n => n + 1);
}}

这里传给 setter 的是更新函数。React 处理队列时,将前一个更新的结果作为下一个更新的输入。初始值为 0 时,计算过程是:

队列中的更新输入值返回值
第一次 n => n + 101
第二次 n => n + 112
第三次 n => n + 123

最终状态因此是 3。更新函数应只计算并返回新值,不要在里面发送请求或执行其他副作用。这些更新队列的行为见 React 官方教程。

“只加一次”和批处理是两个问题

在上面的事件处理中,React 会批量处理状态更新,通常在事件处理代码执行完之后再更新界面。这减少了中间状态带来的重复界面更新。

但批处理不意味着只执行一个更新。函数式写法里的三个更新仍会按顺序计算,最后得到 3。普通写法只得到 1,是因为它向队列提交了三次相同的目标值。

所以不能把现象概括成“连续调用 setter 只会更新一次”。更准确的说法是:当前事件处理函数读取同一份 state 快照,而 setter 可以接收一个目标值,也可以接收基于前一个状态计算的更新函数。

本文计数器根据原学习笔记整理,示例思路来自 React 官方教程,不代表一次个人项目故障复盘。



上一篇
任务系统里的时间:绝对时刻、当地时间与“全天”
下一篇
PostgreSQL 导入数据后,自增主键为什么冲突?