React18 Automatic batching

金虹橋程式設計師發表於2021-11-04

該文選自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>
  );
}

這對效能非常有用,因為它避免了不必要的重新渲染。它還可以防止您的元件呈現僅更新一個狀態變數的“半完成”狀態,這可能會導致錯誤。這可能會讓您想起餐廳服務員在您選擇第一道菜時不會跑到廚房,而是等待您完成訂單。

然而,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 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 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
}

我們不希望這種場景經常出現。

對 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...

相關文章