该文选自React作者 Dan Abramov公布的高热度issue翻译而来,文末有原文链接。文章艰深易通,可读性也相当不错,因而分享给大家,对React18感兴趣的小伙伴欢送学习探讨。

概述

React 18 通过默认执行更多批处理来减少开箱即用的性能改良,无需在应用程序或库代码中手动批量更新。这篇文章将解释什么是批处理,它以前是如何工作的,以及产生了什么变动。

留神:这是一个咱们不心愿大多数用户须要思考的深刻性能。然而它可能与布道师和react库开发者有亲密关联。

什么是批处理

批处理是 React将多个状态更新分组到单个re-render中以取得更好的性能的操作。

例如,如果你在同一个点击事件中有两个状态更新,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>  );}
  • ✅ 演示:在事件处理程序中反馈 17 个批次。(请留神控制台中的每次点击渲染一次。)

这对性能十分有用,因为它防止了不必要的从新渲染。它还能够避免您的组件出现仅更新一个状态变量的“半实现”状态,这可能会导致谬误。这可能会让您想起餐厅服务员在您抉择第一道菜时不会跑到厨房,而是期待您实现订单。

然而,React 的批量更新工夫并不统一。例如,如果您须要获取数据,而后更新handleClick下面的状态,那么 React不会批量更新,而是执行两次独立的更新。

这是因为 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>  );}

在 React 18 之前,咱们只在 React 事件处理程序期间批量更新。默认状况下,React 中不会对 promise、setTimeout、原生事件处理(native event handlers)或其它React默认不进行批处理的事件进行批处理操作。

什么是主动批处理?

从 React 18的createRoot开始,所有更新都将主动批处理,无论它们来自何处。

这意味着timeouts, promises, native event handlers或任何其余事件内的更新将以与 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>  );}
  • ✅ 演示:React 18createRoot在事件处理之外的批处理!(留神控制台中的每次点击渲染一次!)
  •  演示:React 18 with legacyrender保留了旧的行为(留神控制台中每次点击两次渲染。)
留神:作为采纳 React 18 的一部分,预计您将降级到createRoot。 旧行为 的render存在只是为了更容易地对两个版本进行生产试验。

无论更新产生在何处,React 都会主动批量更新,因而:

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

与此雷同:(setTimeout)

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

与此雷同:(fetch)

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

行为与此雷同:(addEventListener)

elm.addEventListener('click', () => {  setCount(c => c + 1);  setFlag(f => !f);  // React will only re-render once at the end (that's batching!)});
留神:React 仅在平安稳固的场景下才批量更新。
例如,React 确保对于每个用户启动的事件(如单击或按键),DOM 在下一个事件之前齐全更新
例如,这可确保在提交时禁用的表单不能被提交两次。

如果不想进行批处理怎么办?

通常,批处理是平安的,但某些代码可能依赖于在状态更改后立刻从 DOM 中读取某些内容。对于这些用例,您能够应用ReactDOM.flushSync()抉择退出批处理:

import { flushSync } from 'react-dom'; // Note: react-dom, not reactfunction handleClick() {  flushSync(() => {    setCounter(c => c + 1);  });  // React has updated the DOM by now  flushSync(() => {    setFlag(f => !f);  });  // React has updated the DOM by now}

咱们不心愿这种场景经常出现。

对 Hooks 有什么影响吗?

如果您应用 Hooks,咱们心愿主动批处理在绝大多数状况下都能“失常工作”。(如果没有,请通知咱们!)

对 Classes 有什么影响吗?

请记住,React 事件处理程序期间的更新始终是批处理的,因而对于这些更新,没有任何更改。

在类组件中存在边缘状况,这可能是一个问题。

类组件有一个实现的奇怪中央,它能够同步读取事件外部的状态更新。这意味着您将可能在setState的调用之间读取this.state

handleClick = () => {  setTimeout(() => {    this.setState(({ count }) => ({ count: count + 1 }));    // { count: 1, flag: false }    console.log(this.state);    this.setState(({ flag }) => ({ flag: !flag }));  });};

在 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 }));  });};

请参阅代码。

如果这是降级到 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 }));  });};

请参阅代码。

此问题不会影响带有 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)

尽管当您采纳 Hooks 时这种行为可能令人诧异,但它为主动批处理铺平了路线。

unstable_batchedUpdates怎么办?

一些 React 库应用这个未记录的 API 来强制对setState内部事件处理程序进行批处理:

import { unstable_batchedUpdates } from 'react-dom';unstable_batchedUpdates(() => {  setCount(c => c + 1);  setFlag(f => !f);});

这个 API 在 18 中依然存在,但不再须要它了,因为批处理是主动产生的。咱们不会在 18 中删除它,只管在风行的库不再依赖于它的存在之后,它可能会在将来的次要版本中被删除。

原文地址:https://github.com/reactwg/re...