React18 中会自动批量更新,达到更少的 render 次数
TIP
原文链接:https://github.com/reactwg/react-18/discussions/21
作者:gaearon dan
默认情况下,通过自动批量更新,React18 获得了开箱即用的性能提升,移除了在应用或库中需要手动批量更新的情况。
这篇文章将会解释什么是批量更新(batching),它以前是怎么工作的,然后现在哪些东西已经改变了。
注意:这是一个有深度的功能,我们不认为大多数用户需要思考它。然而,教育工作者和库开发者可能需要了解它。
什么是批量更新(batching)?
批量更新是指 React 的多个 state 在一个 re-render 里更新,以获得更好的性能。
例如,如果在一个 click 事件中你有两个 state 更新,React 总是会批量更新它们在一个 re-render 里。如果你执行下面的代码,你将会看到每一次你点击,React 仅仅会执行一次 render,即使你设置了 state 两次。
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // 还不会 re-render
setFlag(f => !f); // 还不会 re-render
// 最后,React 仅会 re-render 一次
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}在 React18 之前,批量更新仅仅在 React 绑定的事件中生效(比如 onClick)。而其它在 Promise 内函数,setTimeout 内函数,使用原生绑定事件(比如 document.addEventListenner),等等,默认情况下不会批量更新。
什么是自动批量更新
在 React18 中使用 createRoot 创建应用,所有的更新都会被批处理,不管它来自哪里。
这意味着在 timeout,promise,原生绑定事件等内的更新,将会和在 React 绑定事件里的更新一样。我们希望这能触发更少的渲染次数,因此你的应用有更好的性能。
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// 等会 React18 会批量处理更新
setCount(c => c + 1);
setFlag(f => !f);
// React 将会 re-render 一次。
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}注意:我们假设在 React18 中你已经迁移到 createRoot。老版本的 render 依旧存在,是为了方便做生产试验。
React 将会自动批量更新,不管更新来自哪里,想这样:
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// re-render 一次
}也只 re-render 一次:
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// re-render 一次
}, 1000);setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// re-render 一次
}, 1000);同
fetch(/*...*/).then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// re-render 一次
})同
elm.addEventListener('click', () => {
setCount(c => c + 1);
setFlag(f => !f);
// re-render 一次
});如果我不想批量更新怎么办?
通常,批量更新是安全的。但是在 state 改变后,一些代码可能需要立即从 dom 读取到一些信息。对于这种情况,你可以使用 ReactDOM.flushSync() 来跳出批量更新。
// 注意:react-dom, 不是 react
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// 现在 React 已经重新渲染了 dom
flushSync(() => {
setFlag(f => !f);
});
// 现在 React 已经重新渲染了 dom
}我们不希望这变得普遍。
这对 Hooks 有什么影响吗
没有。
这对 Classes 有什么影响吗
记住在 React 的事件处理器中更新,总是会批量更新。所以这些更新不会对 Classes 有任何影响。
但在 classs 组件这里有一个边缘情况。这也是仓库的一个 issue。
(18 之前)类组件有一个实现怪癖,在同步事件回调里有可能读取到更新后的状态。这意味着你可以读取 this.state 在两次调用 setState 之间:
// count 默认为 0
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};在 React18 中,不会再有上面这种情况了。因为在 setTimeout 里的所有更新都会被批量操作,React 不会在第一次 setState 后同步渲染结果。(渲染发生在浏览器的下一次运作,所以 render 在这时候还没有发生)
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};但如果你就想获取同步渲染后的结果,可以使用 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 }));
});
};这个 issue 不会影响函数式组件,因为使用 setState 更新数据,不会同步更新 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)unstable_batchedUpdates 是什么?
一些 React 库使用在文档没有说明的 api 来强制 setState 会批量更新:
import { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
setCount(c => c + 1);
setFlag(f => !f);
});这个 api 依旧存在于 18,但它没必要,因为 batching 会自动触发。我们不会在 18 中移除它,但在未来一些流行库不再依赖他的时候,我们可能会在未来的主版本中移除它。