該文選自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 18
createRoot
在事件處理之外的批處理!(注意控制檯中的每次點選渲染一次!) - ? 演示:React 18 with legacy
render
保留了舊的行為(注意控制檯中每次點選兩次渲染。)
注意:作為採用 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 中刪除它,儘管在流行的庫不再依賴於它的存在之後,它可能會在未來的主要版本中被刪除。