为什么在异步函数中调用useState hook的set函数会立即生效?

6

当在同步和异步函数中调用多个useState hook的set函数时,我遇到了不同的行为。

function Test() {
    console.log('app rendering starts.');
    const [a, setA] = useState(1);
    const [b, setB] = useState(11);
    const updateState = () => {
        console.log('\tupdating a starts.');
        setA(2);
        console.log('\tupdating a ends.');
        console.log('\tupdating b starts.');
        setB(12);
        console.log('\tupdating b ends.');
    };
    console.log('app rendering ends.');
    return (
        <div className="App">
            <div>a is {a}</div>
            <div>b is {b}</div>
            <button onClick={() => {
                console.log('--------------sync click--------------');
                updateState();
            }}>Update State a & b Sync</button>
            <button onClick={() => {
                console.log('--------------async click--------------');
                setTimeout(updateState, 0)
            }}>Update State a & b Async</button>
        </div>
    );
}

这两个按钮执行相同的代码,但以不同的方式执行。
同步按钮的结果:

app rendering starts.
app rendering ends.
--------------sync click--------------
    updating a starts.
    updating a ends.
    updating b starts.
    updating b ends.
app rendering starts.
app rendering ends.

异步按钮结果:

app rendering starts.
app rendering ends.
--------------async click--------------
    updating a starts.
app rendering starts.
app rendering ends.
    updating a ends.
    updating b starts.
app rendering starts.
app rendering ends.
    updating b ends.

这是期望的行为吗?
在异步函数中如何实现同步结果?
我在官方文档中没有找到任何提示。
感激不尽,求助!
谢谢!


这是一个非常好的发现。我在Codesandbox https://codesandbox.io/s/gallant-curie-u8zhs?file=/src/App.js:0-1314 上尝试了一下,惊讶地看到了结果。使用sync调用可以一次性运行所有的setState函数,然后重新渲染。而async调用则分为两个部分完成。这确实需要解释一下! - Himanshu Singh
这是预期的行为;它是否被期望取决于你想要做什么。你的意思是如何在异步函数中实现同步结果? - wxker
首先,我不理解为什么会发生这种情况;其次,在每次调用后没有必要重新渲染组件。最好在最后一次重新渲染,就像同步函数一样。 - MRNafisiA
关于您的评论,请参考这个有关v18的讨论:https://github.com/reactwg/react-18/discussions/21 - Tushar Shahi
2个回答

3
根据Github讨论,这似乎是一个已知的事实。
其中一条评论相当明确:
React当前将批处理状态更新(state updates),如果它们是从基于React的事件内部触发的,例如按钮单击或输入更改,则会进行批处理。 如果它们是从React事件处理程序之外触发的,例如setTimeout(),则不会批处理更新。
React在内部使用unstable_batchedUpdates()。 您可以传递回调函数和其中的状态更新将被批处理。 对于超时,承诺和异步函数,这不会自动发生。 因此,它们是从回调之外调用的,并且其中的状态更新不会进行批处理。
import { unstable_batchedUpdates } from "react-dom";

const updateState = () => {
    unstable_batchedUpdates(() => {
      console.log("\tupdating a starts.");
      setA(2);
      console.log("\tupdating a ends.");
      console.log("\tupdating b starts.");
      setB(12);
      console.log("\tupdating b ends.");
    });
  };

沙盒链接


0

您可以使用单个useState()useReducer()钩子调用来更新多个状态值,通过将它们存储在一起而不是分别存储在不同的状态片段中。

const [values, setValues] = useState({a: 1, b: 11});

setValues({a: 2, b: 12});

这是一个针对特定问题的好解决方案,但它有两个主要问题。首先,在我的实际问题中,使用这个解决方案并不容易。它有多个钩子和依赖项,合并状态会导致冗余渲染。其次,编辑代码将会很困难。因为如果你的实现发生变化,你必须更改合并变量,而且这个解决方案会影响从父组件获取的属性。 - MRNafisiA
你可以使用 React.memo()useMemo() 钩子来避免不必要的重新渲染。如果这是你对问题的关注点的话。 - Manish
无论如何,从React 18开始,在超时、Promise、本地事件处理程序或任何其他事件中的所有状态更新将批处理方式与React事件内的更新方式相同。这意味着,同步和异步都一样。在React 18中仍将存在unstable_batchedUpdatesAPI。但是,它可能会在以后的版本中被删除。 参考链接:https://github.com/reactwg/react-18/discussions/21 - Manish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接