概览
react18 通过默认执行更多批处理的方式来增加开箱即用的性能优化,让我们无需在应用程序或库中手动地批量处理或更新代码。这篇文档将解释什么是批处理 ,它在之前是如何工作的 ,以及现在发生了什么变化?,请跟着我一起探索React automatic batching 吧😜
注意:这是一个深入的功能,所以我们不希望大多数开发者需要考虑到这个功能而造成过多的心智负担。但是,它可能和从事教学和编写库的开发者有关。
什么是批处理
批处理是指 React 为了更好的性能而将多个状态更新分组到一个重新渲染进程中。例如,如果你有两个state
在同一个点击事件中需要更新,React 会将这两个state
作为一次 渲染。所以,这就是运行下面的代码后,明明在点击事件中更新了两个state
为什么运行代码的时候只看见了一次渲染😮
js
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // 到这里还没重新渲染
setFlag(f => !f); // 到这里还没重新渲染
// React 只会在结束时才会执行渲染
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
- Demo:React17 中事件处理程序的批处理(每点击一次按钮控制台就会输出console信息---渲染一次)
这对性能是非常好的,因为它避免了不必要的重新渲染。它还可以防止我们的组件在只有一个状态变量更新时出现渲染出"半成品"状态的情况,这就可能会导致错误。就好比,你在餐厅点菜时,服务员不会让你点完一道菜就跑回厨房,而是等你点完所有的菜。
然而,React 在进行批量更新的时间上不会保持一致。例如,如果你需要请求数据,然后更新上面的 handleClick
中的状态,那么 React 不会批量更新,而是执行两个独立的更新。这是因为 React 过去只在浏览器事件(如click)期间进行批量更新,但是现在我们是在事件已经被处理后(在fetch的回调函数中)更新状态:
js
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 17 和更早的版本之前不会在这里批量更新
// 因为这里实在点击事件的回调后运行而不是在回调过程中运行
setCount(c => c + 1); // 重新渲染
setFlag(f => !f); // 重新渲染
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
- Demo:React17 不会批处理外部事件处理程序(注意:每次点击按钮控制台都会出现两次渲染)
直到React18,我们只在React事件处理程序期间进行批量更新。在默认情况下,React 的更新不会在 promises,setTimeout,native 事件处理程序或其它任何事件中进行批处理。😏
什么是自动批处理
从React18的createRoot开始,无论来自那里,所有的更新将会自动批处理 。这意味着在 timeout、promise、native事件处理程序或其它任何事件内的更新将和 React 事件以相同的方式进行批处理。这样可以减少渲染的工作量,从而让应用程序有更好的性能。😘
js
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 将在结束的时候只渲染一次
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
- Demo:React18 使用
createRoot
甚至在事件处理程序外部进行批处理(注意:点击一次按钮只渲染一次) - Demo:React18使用旧版的
render
保留了以前的行为(注意:点击一次按钮会渲染两次)
注意:作为React18采用的一部分,预计升级到
createRoot
,render
的存在只是为了更好的对比两个版本在生产环境下的实验。
React 无论更新在哪里,都会自动批量更新,如:
js
function handleClick(){
setCount(c => c+1);
setFlag(f => !f);
// React 将只在结束的时候渲染一次
}
和下面发生的内容相同:
js
fetch(/*...*/).then(()=>{
setCount(c => c+1);
setFlag(f=>!f);
// React 将只在结束的时候渲染一次
})
和下面发生的内容相同:
js
elm.addEventListener('click', () => {
setCount(c => c + 1);
setFlag(f => !f);
// React 将只在结束的时候渲染一次
});
注意:React 通常只在安全的情况下进行批量更新。例如,React确保对于每个用户发起的事件(如点击事件或者按键事件),DOM在下一个事件之前完全更新,这可以确保用户在提交时禁用的表单不会提交两次。
如果我不想使用批处理怎么办
通常,批处理是安全的,但是有些代码可能依赖于在状态更改后立即从DOM读取某些内容。对于这些用例,我们可以使用ReactDOM.flushSync()
的方式进行批量更新😉:
js
import { flushSync } from 'react-dom';// 注意:这里是react-dom,不是react
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// React 现在已经更新了DOM
flushSync(() => {
setFlag(f => !f);
});
// React 现在已经更新了DOM
}
但是这样的情况并不常见...
这对hooks有什么影响吗
如果我们使用的是Hooks,自动批处理在绝大多数情况下都能"正常工作"🥰
这会破坏类吗
请记住React事件处理程序期间的更新始终是批处理的,因此对于这些更新不会有任何更改。在类组件中可能存在一个边缘情况可能是个问题。类组件有一个实现的怪癖:可以在其中同步读取事件内部的状态更新。这意味着我们能够在下面的情况调用setState
:
js
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
更新的结果---渲染发生在浏览器的下一次tick
(周期)。所以渲染还没有发生:
js
handleClick = () => {
setTimeout(() => {
this.setState(({ count }) => ({ count: count + 1 }));
// { count: 0, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
如果这是你升级到React18的障碍,你可以使用ReactDOM.flushSysnc
来强制更新,但是建议谨慎使用🤔:
js
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
更新现有的变量。
js
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时,这样的行为可能会令人惊讶,但他为自动批处理铺平了道路。
总结
React 18 引入了一个重要的改进,即自动批处理(Automatic Batching)。这一改进旨在通过默认执行更多的批处理操作来优化性能,使得开发者无需在应用程序或库中手动进行批量处理或更新代码。
优点:
- 性能优化:减少了不必要的渲染次数。
- 减少心智负担:不再需要手动管理批处理操作
- 一致性:无论状态更新发生在何处,React 18 都会默认进行批处理。
注意事项:
虽然自动批处理带来了很多好处,但我们也需要注意一些细节。例如,在 React 18 中,如果你使用了 React 的 Concurrent Mode(并发模式),那么批处理的行为可能会有所不同。此外,虽然自动批处理减少了不必要的渲染次数,但在某些情况下,它可能会导致组件状态的更新看起来有些"延迟"。因此,在开发过程中,我们需要仔细测试并验证他们的应用是否按照预期工作。