当前位置: 首页 > 知识库问答 >
问题:

前端 - react 18 的自动批量更新为何失效?

殷烨
2024-12-04
export default function App() {
  const [v, setV] = useState(0);

  const handleClick = async () => {
    console.log(0);
    setV((v) => v + 1);
    Promise.resolve().then(() => {
      console.log(1);
      setV((v) => v + 1);
      console.log(2);
      Promise.resolve().then(() => {
        console.log(3);
      });
    });
  };
  const handleClick2 = async () => {
    console.log(0);
    Promise.resolve().then(() => {
      console.log(1);
      setV((v) => v + 1);
      console.log(2);
      Promise.resolve().then(() => {
        console.log(3);
      });
    });
    setV((v) => v + 1);
  };

  console.log("render");

  return (
    <div className="App">
      <h1 onClick={handleClick}>Hello CodeSandbox</h1>
      <h2>value: {v}</h2>
    </div>
  );
}

执行两个click方法,分别打印
handleClick:
image.png
handleClick2
image.png

为何handleClick中自动批量更新没有生效呢

共有1个答案

公良运锋
2024-12-04

分析 handleClickhandleClick2 的执行顺序

handleClick 函数

const handleClick = async () => {
  console.log(0);
  setV((v) => v + 1); // 状态更新1
  Promise.resolve().then(() => {
    console.log(1);
    setV((v) => v + 1); // 状态更新2
    console.log(2);
    Promise.resolve().then(() => {
      console.log(3);
    });
  });
};

执行顺序

  1. 主任务

    • console.log(0) 输出 0
    • setV((v) => v + 1) 触发状态更新1,但不会立即生效,而是进入更新队列。
    • Promise.resolve().then 进入微任务队列。
  2. 微任务

    • 当前宏任务执行完毕,进入微任务队列,执行 console.log(1)
    • setV((v) => v + 1) 触发状态更新2。
    • 输出 2
    • 再次进入微任务队列,输出 3

由于状态更新1和状态更新2发生在不同的事件循环中,React 无法将它们批处理在一起,因此会触发两次重新渲染。

handleClick2 函数

const handleClick2 = async () => {
  console.log(0);
  Promise.resolve().then(() => {
    console.log(1);
    setV((v) => v + 1);
    console.log(2);
    Promise.resolve().then(() => {
      console.log(3);
    });
  });
  setV((v) => v + 1);
};

执行顺序

  1. 主任务

    • console.log(0) 输出 0
    • Promise.resolve().then 进入微任务队列。
    • setV((v) => v + 1) 触发状态更新1,但不会立即生效,而是进入更新队列。
  2. 微任务

    • 当前宏任务执行完毕,进入微任务队列,执行 console.log(1)
    • setV((v) => v + 1) 触发状态更新2。
    • 输出 2
    • 再次进入微任务队列,输出 3

总结

  • handleClick:批量更新失效,因为状态更新发生在不同的事件循环中,React 无法将它们批处理在一起。
  • handleClick2:批量更新有效,因为状态更新虽然发生在不同的事件循环中,但由于 Promise 的微任务队列执行顺序,React 仍然能够批处理这些更新。

解决

方法一:将所有状态更新放在同一个微任务中

const handleClick = async () => {
  console.log(0);
  setV((v) => v + 1);
  await Promise.resolve();
  console.log(1);
  setV((v) => v + 1);
  console.log(2);
  await Promise.resolve();
  console.log(3);
};

优点

  • 简单直接,不需要额外的库或函数。
  • 保持了代码的异步特性。

缺点

  • 需要在每次状态更新前后添加 await Promise.resolve(),可能会使代码显得冗长。

方法二:使用 ReactDOM.flushSync 强制同步更新

import { flushSync } from 'react-dom';

const handleClick = async () => {
  console.log(0);
  flushSync(() => setV((v) => v + 1));
  Promise.resolve().then(() => {
    console.log(1);
    flushSync(() => setV((v) => v + 1));
    console.log(2);
    Promise.resolve().then(() => {
      console.log(3);
    });
  });
};

优点

  • 强制同步更新,确保批量更新生效。
  • 代码更简洁,不需要在每次状态更新前后添加 await Promise.resolve()

缺点

  • 需要引入 flushSync,增加了对 React DOM 的依赖。
  • 可能会影响性能,因为强制同步更新会阻塞渲染。

结论

  • 方法一:如果希望保持代码的异步特性并且不介意稍微冗长的代码,可以选择这种方法。
  • 方法二:如果更注重代码简洁性和确保批量更新生效,可以选择这种方法。

补充

JavaScript 事件循环说明和示例解析:

JavaScript 事件循环

JavaScript 使用事件循环来处理异步操作,通过将任务分为两类:宏任务(macro task)和微任务(micro task)。

宏任务(Macro Task)

宏任务是较大的任务,常见的例子包括:

  • setTimeout
  • setInterval
  • I/O 操作

微任务(Micro Task)

微任务是较小的任务,常见的例子包括:

  • Promise 回调
  • MutationObserver

执行顺序

  1. 执行一个宏任务。
  2. 执行所有的微任务。
  3. 更新渲染。
  4. 重复上述步骤。

示例

console.log('start');

setTimeout(() => {
  console.log('macro task1');
}, 0);

Promise.resolve().then(() => {
  console.log('micro task2');
});

console.log('end');

执行顺序

  1. console.log('start') 立即执行,输出 start

    • 输出:start
  2. setTimeout 是一个宏任务,放入宏任务队列,等待当前任务完成后执行。
  3. Promise.resolve().then 是一个微任务,放入微任务队列,等待当前任务完成后执行。
  4. console.log('end') 立即执行,输出 end

    • 输出:end
  5. 当前宏任务完成,开始执行微任务队列中的任务,输出 micro task2

    • 输出:micro task2
  6. 微任务队列清空后,开始执行宏任务队列中的任务,输出 macro task1

    • 输出:macro task1

所以最终的输出顺序是:

  1. start
  2. end
  3. micro task2
  4. macro task1

这种执行顺序确保了微任务能够在宏任务之前完成,从而保持事件循环的高效运转。这也解释了为什么在Promise中的状态更新会影响批量更新机制的行为。

 类似资料:
  • 一、简介 方便用户在设置URL规则时更新url,无需手动去更新各页面URL。只需选择就能批量更新URL即可。 二、功能演示 1.批量更新URL 1、仅当内容页URL规则发上变化时,请批量更新,地址未改变时,不需要更新 。 2、规则设置:设置 > 相关设置 > 管理栏目 > 添加或修改栏目 选择需要更新的模型和栏目,输入每轮更新的信息数目提交则完成批量更新URL操作。如下图所示:

  • 一、简介 主要用来操作黄页模块的相关更新操作 ! 1、仅当启用、关闭伪静态时,请批量更新所有链接地址,除此,不需要更新 。 2、启用伪静态在模块配置里。 3、更新企业URL,只需选中企业库模型即可,不必选分类。 二、功能演示

  • 我有两个结构相同的表,我想使用另一个表的数据更新一个表,匹配主键。SQLite有一个with(CTE)语句,但以下语句不起作用(sqlite3 v.3.29.0): 我尝试过使用“选择main.ID作为ID,选择temp.Desc作为Desc”,但得到了相同的错误消息。

  • 我有一个在< code>postgresql数据库上使用< code>typeorm的更新查询,如下所示,该查询频繁地在20个项目的列表上执行(每30秒一次)。大约需要。更新12秒,对我的极限来说已经很多了。 是否有可能在单个查询中执行这样的批量更新,而不是迭代其他项?如果是的话-怎么做? 和对于每个项目都是唯一的。

  • 问题内容: 我想用Django更新表格-原始SQL中的内容如下: 我的第一个结果是这样的-但这很讨厌,不是吗? 有没有更优雅的方式? 问题答案: UPD Django 2.2版本现在具有bulk_update。 请参阅以下django文档部分 一次更新多个对象 简而言之,你应该可以使用: 你还可以使用F对象来执行诸如增加行数之类的操作: 请参阅文档:https : //docs.djangopro

  • 我使用的是Play framework 1.2.5和Play-Morphia模块。我想知道是否有一种方法可以在一个Morphia查询中更新许多对象。我在https://github.com/greenlaw110/play-morphia/blob/master/documentation/manual/crud.textile中找到了这个示例,但是我似乎不能在norder中使用“in”操作来查找

  • 使用React封装的图表组件,如何实现自动更新图表数据?

  • autoUpdater模块为Squirrel框架提供了一个接口。 进程: 主进程​ 您可以使用这些项目之一进行快速启动多平台发布服务器以分发应用程序: nuts:为您的应用程序使用智能版本服务器,使用GitHub作为后端。使用Squirrel(Mac和Windows)自动更新 electron-release-server:功能齐全,自主托管的electron应用程序的发布服务器,兼容自动更新器