React 18.3.1 中的 SetState 调用未在 Promise 调用中进行批处理

问题描述 投票:0回答:1

在 React 中,setState 调用通常是批处理的。但是,当包含 microTask(例如 Promise)的本机事件处理程序中发生 setState 调用时,批处理行为会发生变化。具体来说,如果首先在处理程序的同步部分中进行 setState 调用,然后进行具有状态更新的 microTask 操作,然后在 microTask 后进行另一个 setState 调用,则这些 setState 调用不会按预期进行批处理。 Promise 内部的状态更新不是批量的。

反应版本:18.3.1

重现步骤

我在下面附加了一个代码沙箱来重现该问题。您可以运行代码并观察呈现的计数值,该值与预期的 React 行为不一致。

代码示例链接:

https://stackblitz.com/edit/vitejs-vite-qhrv9o?file=src%2FApp.tsx,src%2FCounter.tsx&terminal=dev

预期行为

Promise 内部的状态更新调用应该是批量的,导致增量点击后计数值为 1。

reactjs react-hooks promise event-handling react-state
1个回答
0
投票

...然后在 microTask

 之后进行另一个 
setState

调用

这是一个误会。该调用不会在微任务之后进行。

重点是,您在

setCount
回调之外进行的
then
调用都在同一个同步执行流中执行。仅当调用堆栈为空时才会执行
then
回调。只要当前运行的点击处理程序中有要执行的代码,情况就不是这样。

这就是代码中发生的事情的顺序:

  • 执行点击处理程序
  • 打印“点击处理程序开始”
  • 读取
    countRef.current
    ,即0
  • 第一次调用
    setCount
    ,参数为 0:批量更新。
  • 打印“第一次设置计数后”
  • 创建一个已解决的 Promise 并调用其
    then
    方法。提供给它的回调尚未执行。该回调作为微任务排队。
  • 读取
    countRef.current
    ,仍然是0,因为之前的更新是批量的。
  • 第二次调用
    setCount
    ,再次使用参数 0:此更新也是批处理的。
  • 打印“最后一次设置计数后”

点击处理程序返回,调用堆栈变空。现在,JS 引擎找到了将异步执行批量更新的 React 微任务。这导致

countRef.current
更新为 1。

还没有渲染周期可以执行,因为我们自己的与 Promise 相关的微任务现在轮到执行了:

  • 读取
    countRef.current
    ,现在为1,因为之前的批量更新已经执行了。
  • 第三次调用
    setCount
    ,现在使用参数 1:此更新是批量更新。
  • 打印“承诺内”

then
回调返回并且调用堆栈变空。 JS 引擎找到另一个 React 微任务来执行新批量的更新(这次只有一个)。这导致
countRef.current
更新为 2。

调用堆栈再次为空,最终执行绘制作业,渲染 2。请注意,您永远不会看到显示 1,因为这是执行单击处理程序后运行的第一个绘制作业。

如果您移动了如下代码,批处理和渲染的行为不会有所不同:

  useLayoutEffect(() => {
    countRef.current = count;
    if (buttonRef.current) {
      buttonRef.current.onclick = () => {
        console.log('Click handler start');
        setCount(countRef.current + 1);
        console.log('After first setCount');

        setCount(countRef.current + 1);       // Moved this block here
        console.log('After last setCount');

        Promise.resolve().then(() => {        // ...and only then this block
          setCount(countRef.current + 1);
          console.log('Inside Promise');
        });

      };
    }
  });

无论您将

then
调用放在单击处理程序主体的开头、中间还是末尾,都不会影响这些
setCount
调用的执行顺序,也不会影响渲染。

你写道:

Promise 内部的状态更新调用应该是批量的,导致增量点击后计数值为 1。

批处理的,但是after反应已经清除了由点击处理程序的同步部分收集的批处理。请注意,在该单击处理程序中进行的两次

setCount
调用都是作为同一同步流的一部分执行的。当点击处理程序返回时,JS 引擎开始执行反应作业来处理批量更新。这发生在您自己的
then
回调出队并执行之前。

© www.soinside.com 2019 - 2024. All rights reserved.