【译】自动批处理深潜

548 阅读8分钟

原文链接: github.com/reactwg/rea…

Automatic batching for fewer renders in React 18
React 18 adds out-of-the-box performance improvements by doing more batching by default, removing the need to manually batch updates in application or library code. This post will explain what batching is, how it previously worked, and what has changed.

在React 18中为更少的渲染进行自动批处理

React 18 通过在默认情况下做更多的批处理,增加了开箱即用的性能改进,消除了在应用程序或库代码中手动批处理更新的需要。这篇文章将解释什么是批处理,它以前是如何工作的,以及发生了什么变化。

Note: this is an in-depth feature that we don’t expect most users to need to think about. However, it may be relevant to educators and library developers.

注意:这是一个深入的特性,我们不希望大多数用户刻意去考虑。然而,它可能与教育工作者和库开发人员有关。

What is batching?
Batching is when React groups multiple state updates into a single re-render for better performance.
For example, if you have two state updates inside of the same click event, React has always batched these into one re-render. If you run the following code, you’ll see that every time you click, React only performs a single render although you set the state twice:

批处理是什么?

批处理是指 React 将多个状态更新分组到一个单独的重新渲染中以获得更好的性能。例如,如果你在同一个点击事件中有两个状态更新,React 总是将这些更新批量处理为一个重新渲染。如果您运行以下代码,您将看到每次单击时,React 只执行一次渲染,尽管您设置了两次状态:

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    setCount(c => c + 1); // Does not re-render yet
    setFlag(f => !f); // Does not re-render yet
    // React will only re-render once at the end (that's batching!)
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

This is great for performance because it avoids unnecessary re-renders. It also prevents your component from rendering “half-finished” states where only one state variable was updated, which may cause bugs. This might remind you of how a restaurant waiter doesn’t run to the kitchen when you choose the first dish, but waits for you to finish your order.

这对于性能非常好,因为它避免了不必要的重新渲染。它还可以防止组件渲染只更新了一个状态变量的半成品状态,这可能会导致bug。这可能会提醒你,当你点第一道菜时,餐厅服务员不会立即送到厨房,而是等待你点完。

However, React wasn’t consistent about when it batches updates. For example, if you need to fetch data, and then update the state in the handleClick above, then React would
not
batch the updates, and perform two independent updates.

然而,React 在批量更新的时机并不一致。例如,如果你需要获取数据,然后在上面的 handleClick 中更新状态,那么React 将不会批量更新,而是执行两个独立的更新。

This is because React used to only batch updates
during
a browser event (like click), but here we’re updating the state
after
the event has already been handled (in fetch callback):

这是因为 React 通常只在浏览器事件中进行批量更新(比如点击),但在这里,我们在事件被处理后更新状态(在 fetch 的回调函数中)。

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      // React 17 and earlier does NOT batch these because
      // they run *after* the event in a callback, not *during* it
      setCount(c => c + 1); // Causes a re-render
      setFlag(f => !f); // Causes a re-render
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

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.

在 React 18之前,我们只在 React 事件处理程序中批量更新。默认情况下,promise、setTimeout、原生事件处理程序或任何其他事件中的更新都没有在 React 中批量更新。

What is automatic batching?
Starting in React 18 with createRoot, all updates will be automatically batched, no matter where they originate from.
This means that updates inside of timeouts, promises, native event handlers or any other event will batch the same way as updates inside of React events. We expect this to result in less work rendering, and therefore better performance in your applications:

什么是自动批处理?

从 React 18 的 createRoot 开始,所有的更新都会自动批处理,不管它们来自哪里。

这意味着 timer、promises、原生事件处理程序或任何其他事件中的更新将以与 React 事件中的进行更新相同的批处理。我们希望这会减少渲染工作,从而提高应用程序的性能

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);

  function handleClick() {
    fetchSomething().then(() => {
      // React 18 and later DOES batch these:
      setCount(c => c + 1);
      setFlag(f => !f);
      // React will only re-render once at the end (that's batching!)
    });
  }

  return (
    <div>
      <button onClick={handleClick}>Next</button>
      <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
    </div>
  );
}

Note: It is expected that you will upgrade tocreateRoot as part of adopting React 18. The old behavior with render only exists to make it easier to do production experiments with both versions.
React will batch updates automatically, no matter where the updates happen, so this:

注意:在使用 React 18 的过程中,你会升级到 createroot。旧的渲染行为的存在只是为了让它更容易做生产实验的两个版本。

React 会自动批量更新,不管更新发生在哪里

// 合成事件

function handleClick() {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}

behaves the same as this:

// timer

setTimeout(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
}, 1000);

behaves the same as this:

// 回调函数

fetch(/*...*/).then(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
})

behaves the same as this:

// 原生事件中

elm.addEventListener('click', () => {
  setCount(c => c + 1);
  setFlag(f => !f);
  // React will only re-render once at the end (that's batching!)
});

Note: React only batches updates when it’s generally safe to do. For example, React ensures that for each user-initiated event like a click or a keypress, the DOM is fully updated before the next event. This ensures, for example, that a form that disables on submit can’t be submitted twice.

注意: React 只有在通常安全的情况下才对批量更新。

例如,React 确保每个用户行为,比如单击或按键,DOM 在下一次事件之前都被完全更新

这可以确保,例如,一个在提交时禁用的表单不能被提交两次。

What if I don’t want to batch?
Usually, batching is safe, but some code may depend on reading something from the DOM immediately after a state change. For those use cases, you can use ReactDOM.flushSync() to opt out of batching:

如果我不想批处理呢?

通常情况下,批处理是安全的,但有些代码可能依赖于在状态更改后立即从 DOM 读取某些内容。

对于这些用例,您可以使用 ReactDOM.flushSync() 选择不进行批处理

import { flushSync } from 'react-dom'; // Note: react-dom, not react

function handleClick() {
  flushSync(() => {
    setCounter(c => c + 1);
  });
  // React has updated the DOM by now
  flushSync(() => {
    setFlag(f => !f);
  });
  // React has updated the DOM by now
}

We don't expect this to be common.

我们不认为这是普遍现象。

Does this break anything for Hooks?
If you’re using Hooks, we expect automatic batching to "just work" in the vast majority of cases. (Tell us if it doesn't!)

这对 Hooks 是破坏性的更新吗?

如果您使用的是 Hooks,我们希望自动批处理在绝大多数情况下能够“正常工作”。(如果没有,请告诉我们!)

Does this break anything for Classes?
Keep in mind that updates
during
React event handlers have always been batched, so for those updates there are no changes.
There is an edge cases in class components where this can be an issue.
Class components had an implementation quirk where it was possible to synchronously read state updates inside of events. This means you would be able to read this.state between the calls to setState:

这对 Class 有什么影响吗?

请记住,在 React 事件处理期间的更新一直是批处理的,所以这些更新不会有任何变化。

在类组件的边缘情况下,这可能是一个问题。类组件有一个实现怪癖,可以在事件内部同步读取状态更新。这意味着你能读懂这个。在 setState 调用之间

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

In React 18, this is no longer the case. Since all of the updates even in setTimeout are batched, React doesn’t render the result of the first setState synchronously—the render occurs during the next browser tick. So the render hasn’t happened yet:

在 React 18 中,情况不再是这样了。因为 setTimeout 中的所有更新都是批处理的,所以 React 不会同步渲染第一个 setState 的结果,渲染发生在下一个浏览器周期。所以渲染还没有发生

handleClick = () => {
  setTimeout(() => {
    this.setState(({ count }) => ({ count: count + 1 }));

    // { count: 0, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

See sandbox.
If this is a blocker to upgrading to React 18, you can use ReactDOM.flushSync to force an update, but we recommend using this sparingly:

如果这阻碍了 React 18 的升级,你可以使用 ReactDOM.flushSync 强制更新,但我们建议谨慎使用

handleClick = () => {
  setTimeout(() => {
    ReactDOM.flushSync(() => {
      this.setState(({ count }) => ({ count: count + 1 }));
    });

    // { count: 1, flag: false }
    console.log(this.state);

    this.setState(({ flag }) => ({ flag: !flag }));
  });
};

See sandbox.
This issue doesn't affect function components with Hooks because setting state doesn't update the existing variable from useState:

这个问题不会影响到使用 Hooks 的函数组件,因为设置状态不会从 useState 更新现有的变量

function handleClick() {
  setTimeout(() => {
    console.log(count); // 0
    setCount(c => c + 1);
    setCount(c => c + 1);
    setCount(c => c + 1);
    console.log(count); // 0
  }, 1000)

While this behavior may have been surprising when you adopted Hooks, it paved the way for automated batching.

当您采用 hook 时,这种行为可能会令人惊讶,但它为自动批处理铺平了道路。

What about unstable_batchedUpdates?
Some React libraries use this undocumented API to force setState outside of event handlers to be batched:

unstable_batchedUpdates呢?

一些 React 库使用这个没有文档化的 API 来强制事件处理程序之外的 setState 被批处理:

import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
});

This API still exists in 18, but it isn't necessary anymore because batching happens automatically. We are not removing it in 18, although it might get removed in a future major version after popular libraries no longer depend on its existence.

这个 API 在 18 中仍然存在,但已经没有必要了,因为批处理是自动发生的。我们不会在 18 中删除它,尽管它可能会在未来的主要版本中删除,因为流行的库不再依赖于它的存在。