React 18 新特性之自动批量更新
1 什么是批量更新
1.1 概念
批量更新(Batching):将多次 state 更新合并到一次 render。
1.2 优点
- 提升性能;
- 避免意外bug。当需要更新多个state时,如果每更新一个 state 就 render,或将会出现“半完成”的情况,可能造成异常。
1.3 举例
举例来看什么是批量更新:Demo地址
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
// re-render 一次
setCount((c) => c + 1); setFlag((f) => !f); }
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
<LogEvents />
</div>
);
}
function LogEvents(props) {
console.log("Render");
return null;
}
// 结果:点击一次 button,只会打印一个"Render"
2 批量更新的时机
2.1 差异
React 18 对批量更新的时机做了调整:
- 在 React 18 之前,React 仅在浏览器事件处理期间才会做批量更新,而在其他事件(比如:Promise、setTimeout、native 事件)期间是不会执行的。
-
在 React 18 版本中,并且使用
ReactDOM.createRoot
,则 React 会在所有事件中进行自动批量更新(Automatic Batching) 。而如果使用ReactDOM.render
,则会依旧维持之前版本的表现。
备注:为了介绍起来方便,后文 React 18 均代指 React 18+使用 createRoot
的情况。
2.2 举例
React 18 之前:Demo 地址
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setTimeout(() => { setCount((c) => c + 1); setFlag((f) => !f); }, 0);
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
<LogEvents />
</div>
);
}
function LogEvents(props) {
console.log("Render");
return null;
}
// 结果:点击一次 button,会打印两个"Render"
React 18:createRoot Demo 地址
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setTimeout(() => { setCount((c) => c + 1); setFlag((f) => !f); }, 0);
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
<LogEvents />
</div>
);
}
function LogEvents(props) {
console.log("Render");
return null;
}
const rootElement = document.getElementById("root");
// 结果:React 18 新特性。点击一次 button,只会打印一个"Render" ReactDOM . createRoot(rootElement).render(<App />);
// 结果:维持之前的表现。点击一次 button,会打印两个"Render"
// ReactDOM.render(<App />, rootElement);
2.3 不同事件中的 state 更新
看完上文,不知你是否会产生一个新的疑问 —— 对于不同事件里的 state 更新,React 18 会做自动批处理吗?
于是我们尝试了几种其他的情形:Demo 地址
function handleClick() {
setTimeout(() => {
setCount((c) => c + 1);
}, 0);
setTimeout(() => {
setFlag((f) => !f);
}, 0);
}
// 结果:点击一次 button,只会打印一个"Render"
function handleClick() {
setTimeout(() => {
setCount((c) => c + 1);
}, 0);
setTimeout(() => {
setFlag((f) => !f);
}, 100);
}
// 结果:点击一次 button,会打印两个"Render"
function handleClick() {
setCount((c) => c + 1);
setTimeout(() => {
setFlag((f) => !f);
}, 0);
}
// 结果:点击一次 button,会打印两个"Render"
React 只在安全的情况下批量更新,会确保对于发起的事件,DOM 在下一个事件之前完全更新。
3 如果不想使用自动批量更新
如果既想使用 React 18 ,但又不想使用自动批量更新这个特性(非必要情况不太建议这么做)。可以选择这么做:
3.1 策略一:使用 render
3.1 节中,我们提到“React 18 中只有使用 ReactDOM.createRoot
才会触发自动批量更新”。相应的,如果使用ReactDOM.render
,则会依旧维持之前版本的表现。
-
举例: render Demo 地址
3.2 策略二:使用 flushSync
可以使用 ReactDOM.flushSync
将 state 更新分开包裹
举例:Demo 地址
import { flushSync } from 'react-dom'; // Note: react-dom, not react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
flushSync(() => {
setFlag(f => !f);
});
}
// 结果:点击一次 button,会打印两个"Render"
4 特殊 case & 注意点
4.1 在类组件的异步事件中更新 state
之前,Class 组件在异步事件中更新 state,会有一个边界 case,如下:
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
在 React 18 中,因为异步事件中的 state 更新也做了批处理,因此解决了这个边界 case
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
但是,如果你希望 React 18 中,类组件的这个 case 依旧保持,你可以使用上文提到的flushSync
。
建议:非必要不要这么做
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
4.2 unstable_batchedUpdates
React 18 之前有个冷门 APIunstable_batchedUpdates
,可以用它来做批量更新,现在 React 18 已经支持自动批量更新了,因此没必要使用它了。React 暂时不会移除这个 API,但未来可能会移除。
5 总结
React 18 之前只会对浏览器事件内的 state 更新做批处理,但 React 18 起能够对所有事件(浏览器事件、异步事件等)的 state 更新做自动的批处理。
参考文档
转载自:https://juejin.cn/post/7153814771937067044