使用钩子时,React 批处理状态更新功能是否正常?

Does React batch state update functions when using hooks?

对于 class 个组件,this.setState 如果在事件处理程序中则调用批处理。但是,如果在事件处理程序之外更新状态并使用 useState 挂钩会发生什么?

function Component() {
  const [a, setA] = useState('a');
  const [b, setB] = useState('b');

  function handleClick() {
    Promise.resolve().then(() => {
      setA('aa');
      setB('bb');
    });
  }

  return <button onClick={handleClick}>{a}-{b}</button>
}

它会立即呈现 aa - bb 吗?或者它将是 aa - b 然后 aa - bb?

TL;DR – 如果状态变化是异步触发的(例如包装在一个承诺中),它们将不会被批处理;如果直接触发,会被批量处理。

我已经设置了一个沙箱来试试这个:https://codesandbox.io/s/402pn5l989

import React, { Fragment, useState } from 'react';
import ReactDOM from 'react-dom';

import './styles.css';

function Component() {
  const [a, setA] = useState('a');
  const [b, setB] = useState('b');
  console.log('a', a);
  console.log('b', b);

  function handleClickWithPromise() {
    Promise.resolve().then(() => {
      setA('aa');
      setB('bb');
    });
  }

  function handleClickWithoutPromise() {
    setA('aa');
    setB('bb');
  }

  return (
    <Fragment>
    <button onClick={handleClickWithPromise}>
      {a}-{b} with promise
    </button>
    <button onClick={handleClickWithoutPromise}>
      {a}-{b} without promise
    </button>
      </Fragment>
  );
}

function App() {
  return <Component />;
}

const rootElement = document.getElementById('root');
ReactDOM.render(<App />, rootElement);

我制作了两个按钮,一个触发包裹在您的代码示例中的 promise 中的状态更改,另一个直接触发状态更改。

如果你看一下控制台,当你点击“with promise”按钮时,它会首先显示 a aab b,然后是 a aab bb .

所以答案是否定的,在这种情况下,它不会立即渲染aa - bb,每个状态变化都会触发一个新的渲染,没有批处理。

但是,当您点击“without promise”按钮时,控制台会立即显示a aab bb

所以在这种情况下,React 会批处理状态更改并同时对两者进行一次渲染。

目前在 React v16 及更早版本中,默认情况下仅对 clickonChange 等 React 事件处理程序内部的更新进行批处理。所以就像 类 状态更新在 hooks

中以类似的方式进行批处理

有一个不稳定的 API 在极少数情况下强制在事件处理程序之外进行批处理。

ReactDOM.unstable_batchedUpdates(() => { ... })

有计划在未来版本中对所有状态更新进行批处理,可能是 v17 或更高版本。

现在,如果事件处理程序中的状态更新调用在异步函数中或由于异步代码而触发,它们将不会被批处理,而直接更新将被批处理

在没有同步代码的情况下,状态更新是批处理的,而异步代码更新不是

function App() {
  const [count1, setCount1] = useState(0);
  const [count2, setCount2] = useState(0);

  // async update from useEffect
  useEffect(() => {
    setTimeout(() => {
      setCount1(count => count + 1);
      setCount2(count => count + 2);
    }, 3000);
  }, []);

  const handleAsyncUpdate = async () => {
    await Promise.resolve("state updated");
    setCount1(count => count + 2);
    setCount2(count => count + 1);
  };

  const handleSyncUpdate = () => {
    setCount1(count => count + 2);
    setCount2(count => count + 1);
  };

  console.log("render", count1, count2);
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <button type="button" onClick={handleAsyncUpdate}>
        Click for async update
      </button>
      <button type="button" onClick={handleSyncUpdate}>
        Click for sync update
      </button>
    </div>
  );
}

https://codesandbox.io/s/739rqyyqmq

如果事件处理程序是 react-based,那么它会分批更新。对于 setState 或 useState 调用都是如此。

但如果事件是基于 non-react 的,即 setTimeout,Promise 调用,它不会自动批处理。简而言之,来自 Web APIs.

的任何事件

@Patrick Hund 已经给出答案.. 只是想在这里更新一下,使用 React 18 批处理状态更新可以用于 Promise,默认情况下也可以设置超时。

Until React 18, we only batched updates during the React event handlers. Updates inside of promises, setTimeout, native event handlers, or any other event were not batched in React by default.

查看此内容以获取详细说明。 https://github.com/reactwg/react-18/discussions/21

React 18 with createRoot,自动批处理所有更新,无论它们来自何处。

请注意,具有旧版 ReactDOM.render() 的 React 18 保留了旧行为。如果您想在超时、承诺或任何其他事件内批量更新,请使用 ReactDOM.createRoot()

这里我们在超时内更新状态两次,但 React 只渲染一次:

import React, { useState } from "react";
import ReactDOM from "react-dom";

function App() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);

  function handleClick() {
    setTimeout(() => {
      setX((p) => p + 1);
      setY((p) => p + 1);
    }, 100);
  }

  console.log(`render x: ${x} y: ${y}`);

  return (
    <div className="App">
      <button onClick={handleClick}>Update with promise</button>

      <div>X: {x} </div>
      <div>Y: {y} </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById("root")).render(<App />);