什么是批處理
批處理是指React將多個狀態(tài)更新組合為單個新渲染,以獲得更好的性能
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // 還不會重新渲染
setFlag(f => !f); // 還不會重新渲染
// React只會在結(jié)束處渲染一次 (這就是batching!)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
react17 批處理示例(注意觀察控制臺每次點擊事件對應(yīng)的一次渲染)
react17 非批處理示例(注意觀察控制臺每次點擊事件對應(yīng)的兩次渲染)
對比可以看到,在異步事件中梁钾,react17批處理失效了赊抖,這正是react18提出自動批處理的由來
什么是自動批處理婴噩?
從React 18的createRoot開始(以下說的react18能夠批處理都指的默認使用了createRoot)盾鳞,所有更新都會自動批處理灸姊,不管它們來自哪里爬虱。
這意味著 timeouts, promises, native event處理程序或任何其他事件中的更新將以與React事件中的更新相同的方式進行批處理隶债。我們希望這樣可以減少渲染的次數(shù),從而提高應(yīng)用程序的性能跑筝。
react18 使用createRoot批處理示例(注意觀察控制臺每次點擊事件對應(yīng)的一次渲染)
react18 不使用createRoot的行為示例(注意觀察控制臺每次點擊事件對應(yīng)的兩次渲染)
什么是 unstable_batchedUpdates?
一些React庫使用這個非正式的API強制setState在事件處理程序之外進行批處理:
import { unstable_batchedUpdates } from 'react-dom';
function handleClick() {
fetchSomething().then(() => {
unstable_batchedUpdates(()=>{
setCount((c) => c + 1);
setFlag((f) => !f);
})
});
}
這個API在18中仍然存在死讹,但它不再是必要的,因為批處理是自動發(fā)生的曲梗。我們不會在18中刪除它赞警,盡管它可能會在未來的主流庫不再依賴它的存在后被刪除。
如果我不需要批處理呢虏两?
通常愧旦,批處理是安全的,但有些代碼可能依賴于在狀態(tài)更改后立即從DOM中讀取某些內(nèi)容定罢。對于這些用例笤虫,你可以使用ReactDOM.flushSync()來選擇不做批處理:
import { flushSync } from 'react-dom'; // 注意是react-dom,而不是react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
console.log('渲染了第1次')
flushSync(() => {
setFlag(f => !f);
});
console.log('渲染了第2次')
}
使用Hooks要注意什么?
如果使用hook祖凫,我們希望自動批處理在絕大多數(shù)情況下都能“正常工作”琼蚯。
使用class組件要注意什么?
請記住惠况,在React事件處理程序期間的更新總是批處理的遭庶,因此對于這些更新沒有更改。
在class組件的某些邊界情況下售滤,這可能是一個問題罚拟。
class組件有一個實現(xiàn)特性台诗,可以同步讀取事件內(nèi)部的狀態(tài)更新。這意味著你能夠讀取到setState調(diào)用之間的狀態(tài): 見示例(在/src/app.js中更改REACT_18
的值模擬是否使用react18版本):
state = {
count: 0,
flag: false
}
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
console.log(this.state);// 拿到上一行更新結(jié)果:{ count: 1, flag: false }
this.setState(({ flag }) => ({ flag: !flag }));
});
};
在React 18中赐俗,情況不再是這樣拉队。因為setTimeout中的所有更新都是批處理的,所以React不會同步呈現(xiàn)第一個setState的結(jié)果——呈現(xiàn)發(fā)生在下一次瀏覽器計時期間阻逮。所以渲染還沒有發(fā)生:見示例(在/src/app.js中更改REACT_18
的值模擬是否使用react18版本):
state = {
count: 0,
flag: false
}
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
console.log(this.state); // 上一行此時并未完成:{ count: 0, flag: false }
this.setState(({ flag }) => ({ flag: !flag }));
});
};
如果這是升級到React 18后對你來說是一個阻礙粱快,你可以使用ReactDOM.flushSync來強制更新,但我們建議謹慎使用:
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
console.log(this.state);// 拿到上一行更新結(jié)果:{ count: 1, flag: false }
this.setState(({ flag }) => ({ flag: !flag }));
});
};
這個問題不會影響使用hooks的函數(shù)組件叔扼,因為下一行設(shè)置state不會更新到上一行設(shè)置State的結(jié)果:
function handleClick() {
setTimeout(() => {
console.log(count); // 0
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
console.log(count); // 依然是0
}, 1000)
總結(jié):
升級到react18時事哭,有以下更新:
- 自動批處理建立在使用createRoot的前提
- 通過flushSync在setState之間讀取狀態(tài)時解決批處理問題
- 影響:在 timeouts, promises, native event處理程序或任何其他事件內(nèi)的更新將以與在React事件內(nèi)的更新相同的方式批處理,而在react17中默認不這樣
- 謹慎使用unstable_batchedUpdates