先记住这个答案
传给 setter 的普通值表示用这个值替换待计算状态,传入的 updater 则接收队列中前一步的结果并返回下一步结果。同一批更新需要按顺序组合,而不是把所有普通值都当成累加,也不是永远只保留最后一次调用。直接表达式中的 count 属于当前渲染快照,在调用 setter 时就算出了值;updater 的参数才表示队列推进到那一步的状态。下面用单个普通点击事件中的同优先级更新解释这一模型。
- 直接传值是替换,updater 是以前一步结果计算
- 普通表达式先读取当前渲染快照再入队
- 更新函数只返回下一状态,不承担业务副作用
初始为一时为什么最终得到三
第一次点击捕获的 count 是一,所以第三条调用提交的直接值是二。队列依次经历替换为四、加三得到七、替换为二、加一得到三。第三步没有读取刚算出的七,它只是使用事件处理器早已计算好的普通参数。
当界面已经显示三后再点击,第三条提交的值变为六,最终结果就是七。推题时先把普通表达式替换成当前快照对应的常量,再推队列,能够避免把不同时间点的变量混在一起。
import { useState } from 'react';
export default function QueueCounter() {
const [count, setCount] = useState(1);
function run() {
setCount(4);
setCount(value => value + 3);
setCount(count * 2);
setCount(value => value + 1);
}
return <button onClick={run}>队列结果:{count}</button>;
}从一开始点击一次得到三,再点击得到七。中间的七不是第三条普通表达式的输入;只有更新函数的参数会接收队列前一步计算出来的结果。
替换值适合明确覆盖而不是表达相对变化
例如用户选择一个确切的页码,直接提交该页码可以清楚表达覆盖;点击下一页则是在已有页码基础上推进,适合 updater。两种更新混用不一定是错误,但需要有明确的业务先后关系。
如果多个字段必须一起遵守某个状态约束,可考虑合并状态或用 reducer 表达动作。不要试图靠在几个 setter 之间插入日志,推断 React 已经把中间值写回当前组件变量;日志读到的仍可能是同一次快照。
更新函数可能被重新执行,必须没有副作用
开发环境的 Strict Mode 会帮助检查 updater 纯度,不能把“函数被调用几次”当成提交了几次业务操作。发送请求、修改外部数组或生成不可重放的业务标识放进 updater,可能造成重复动作或不稳定结果。
合理的 updater 只依据输入计算返回值,保持输入对象不变。测试应核对最终状态和已提交的界面,并明确是否启用严格模式;把组件函数日志数量当成最终更新次数,会混入开发检查与被放弃的渲染。
容易答错的地方
- 队列中只要有普通值,前面的 setter 就完全没有意义
- 普通替换会覆盖它之前计算出的状态,但前后的更新仍有明确顺序;需要逐项推导,不能概括成所有 setter 只剩最后一条。
- updater 参数就是事件开始时的 count
- updater 接收队列推进到当前步骤的状态,和闭包捕获的 count 不是同一个来源;混用它们正是许多状态计算错误的原因。
面试官还会怎么问?
最后一条改成 setCount(9) 会怎样?
在这个单批次示例里,最后一次直接替换让结果为九,前面的计算结果被覆盖。它仍不能说明其他副作用可以安全地写进前面的 updater。
为什么不能在 updater 里 push 到旧数组?
它会原地修改输入和可能共享的历史引用,重复检查或重新计算时结果会偏离预期。应返回新数组,让相同输入产生相同的下一状态。
这个队列模型是否保证组件函数只调用一次?
不保证,逻辑更新的组合与渲染调用次数是不同层次。严格模式、调度和其他更新都可能影响调用次数,应检查实际提交结果而非仅数日志。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。