2.使用 useState 的函数式更新方式能带来哪些好处? 来源:https://w0hog67yl81.feishu.cn/wiki/QqiywOmgdihx6Lk1uWFcZPQJnYe 采集状态:已完成正文采集;正文滚动至底部,按块及代码行号采集 useState :为什么函数式更新更可靠?​ ​ 在日常的 React 开发中,useState 是我们最亲密的伙伴之一。我们通常会这样使用它:​ ​ const [count, setCount] = useState(0); const handleIncrement = () => { setCount(count + 1); // 直接传入一个新值 }; ​ 这种 setCount(newValue) 的方式直观且易于理解。然而,useState 还提供了另一种看似稍显繁琐的更新模式——函数式更新:​ ​ const [count, setCount] = useState(0); const handleIncrement = () => { setCount(prevCount => prevCount + 1); // 传入一个函数 }; ​ 既然已经有了直接更新的方式,为什么 React 还要提供函数式更新呢?它仅仅是一种语法偏好,还是背后蕴含着更深层的原因?​ ​ 答案是后者。在某些特定场景下,函数式更新不仅是“更好”的选择,甚至是“唯一正确”的选择。它能帮助我们有效规避一类因 State 闭包陷阱(Stale Closure)导致的常见 Bug。​ ​ 问题浮现:当 State 更新不符合预期​ ​ 让我们从一个简单的反例开始。假设我们需要在一次交互中,连续两次将 count 的值加一。很多人会很自然地写出以下代码:​ ​ import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const handleTripleIncrement = () => { setCount(count + 1); setCount(count + 1); setCount(count + 1); }; return (

Count: {count}

); } ​ 直觉上,每次点击按钮,count 应该会增加 3。但实际运行后你会发现,count 每次只增加了 1。​ ​ 这背后的本质原因是:setCount 的更新是异步的,并且 React 会对短时间内连续的更新进行批处理(Batching)。​ ​ 在 handleTripleIncrement 函数的执行上下文中,count 的值始终是当次渲染时的快照。当我们连续三次调用 setCount(count + 1) 时,对于 React 而言,它收到的指令是:​ ​ 1. setCount(0 + 1)​ 2. setCount(0 + 1)​ 3. setCount(0 + 1) 由于这三次更新被批处理,最终的结果是将 count 更新为 1,而不是我们期望的 3。同样的问题也会出现在异步操作中,例如在一个 setTimeout 的回调里去更新一个已经被外部改变的 State。​ ​ 解决方案:使用函数获取最新 State​ ​ 为了解决这个问题,函数式更新应运而生。当我们向 set 函数传递一个函数时,React 会保证这个函数的入参(我们通常命名为 prevState 或 prevCount)**永远是当前最新的 State 值**。​ ​ React 会将这些更新函数维护在一个队列中,并在下一次渲染前依次执行,确保每一次计算都是基于前一次更新后的结果。​ ​ 现在,我们用函数式更新来重构上面的例子:​ ​ import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const handleTripleIncrement = () => { // 每次都基于最新的 state 进行计算 setCount(prevCount => prevCount + 1); setCount(prevCount => prevCount + 1); setCount(prevCount => prevCount + 1); }; return (

Count: {count}

); } ​ 这一次,代码的行为就完全符合预期了。每次点击,count 都会正确地增加 3。React 的处理流程变成了:​ ​ 1. 将 prevCount => prevCount + 1 推入更新队列。​ 2. 再次将 prevCount => prevCount + 1 推入队列。​ 3. 第三次推入队列。​ 4. 在处理阶段,取出第一个函数,执行 0 + 1,结果为 1。​ 5. 取出第二个函数,此时 prevCount 是 1,执行 1 + 1,结果为 2。​ 6. 取出第三个函数,此时 prevCount 是 2,执行 2 + 1,结果为 3。​ 7. 最终,将 count 的状态更新为 3。 函数式更新的核心优势​ ​ 综上所述,我们可以总结出函数式更新带来的几个关键好处:​ ​ 1. 保证 State 更新的准确性:这是最核心的优势。它能确保我们的更新逻辑始终基于最新的 State,从而彻底避免因 State 闭包陷阱或批处理带来的数据不一致问题。​ 2. 代码逻辑更具可预测性:当看到 setCount(c => c + 1) 这样的代码时,我们能立刻明白开发者的意图是“在当前值的基础上加一”。这种更新方式是自包含的,不依赖外部的变量,降低了代码的耦合度和出错的可能性。​ 3. 优化自定义 Hook 的设计:在编写自定义 Hook 时,有时我们只需要向外暴露 State 的更新能力,而不需要暴露 State 本身。函数式更新使得我们可以只传递 set 函数,而无需同时传递 State 值,从而让 Hook 的接口设计更加简洁和安全。 最佳实践:我们该如何选择?​ ​ 那么,在实际开发中,我们应该遵循什么样的原则呢?一个简单有效的经验法则是:​ ​ • 如果新的 State 依赖于旧的 State,请始终使用函数式更新。 ◦ 计数器:setCount(prev => prev + 1) ◦ 布尔值切换:setToggle(prev => !prev) ◦ 数组操作:setItems(prev => [...prev, newItem]) • 如果新的 State 与旧的 State 完全无关,可以直接传入新值。 ◦ 表单输入:setName(e.target.value) ◦ 重置状态:setCount(0) 结论​ ​ 函数式更新并非只是一个可有可无的语法糖,而是 React 为我们提供的一个用于编写更健壮、更可预测组件的强大工具。养成在“基于旧值计算新值”的场景下使用函数式更新的习惯,将大大减少我们应用中潜在的 Bug,提升代码的整体质量。​ ​