react18的批处理渲染(翻译)
React 18 通过默认执行更多批处理来增加开箱即用的性能改进,从而无需在应用程序或库代码中手动批量更新。这篇文章将解释什么是批处理、它以前如何工作以及发生了什么变化。
注意:这是一个深入的功能,我们不期望大多数用户需要考虑这个问题。然而,这对于教育工作者和库开发人员可能是相关的。
什么是批处理?
批处理是指 React 将多个状态更新分组到单个重新渲染中以获得更好的性能。
例如,如果同一点击事件中有两个状态更新,React 总是将它们批处理为一次重新渲染。如果运行以下代码,您将看到每次单击时,尽管您设置了两次状态,React 只执行一次渲染:
1function App() { 2 const [count, setCount] = useState(0); 3 const [flag, setFlag] = useState(false); 4 5 function handleClick() { 6 setCount(c => c + 1); // Does not re-render yet 7 setFlag(f => !f); // Does not re-render yet 8 // React will only re-render once at the end (that's batching!) 9 } 10 11 return ( 12 <div> 13 <button onClick={handleClick}>Next</button> 14 <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> 15 </div> 16 ); 17}
✅ Demo: React 17 batches inside event handlers.(注意在控制台中每次点击只会渲染一次)
这对于性能来说非常有用,因为它避免了不必要的重新渲染。它还可以防止您的组件呈现仅更新一个状态变量的“半完成”状态,这可能会导致错误。这可能会让你想起当你选择第一道菜时,餐厅服务员不会跑到厨房,而是等待你完成你的订单。
然而,React 在批量更新方面并不一致。例如,如果你需要获取数据,然后更新上面 handleClick
中的状态,那么React不会批量更新,而是执行两次独立的更新。
这是因为 React 过去只在浏览器事件(如单击)期间批量更新,但这里我们在事件处理完成后更新状态(在 fetch 回调中):
1function App() { 2 const [count, setCount] = useState(0); 3 const [flag, setFlag] = useState(false); 4 5 function handleClick() { 6 fetchSomething().then(() => { 7 // React 17 and earlier does NOT batch these because 8 // they run *after* the event in a callback, not *during* it 9 setCount(c => c + 1); // Causes a re-render 10 setFlag(f => !f); // Causes a re-render 11 }); 12 } 13 14 return ( 15 <div> 16 <button onClick={handleClick}>Next</button> 17 <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> 18 </div> 19 ); 20}
🟡 Demo: React 17 does NOT batch outside event handlers. (注意控制台里的每次点击都会渲染两次)
在 React 18 之前,我们仅在 React 事件处理程序期间批量更新。默认情况下,React 中不会批处理 Promise、setTimeout、原生事件处理程序或任何其他事件内部的更新。
什么是自动批量?
从 React 18 开始,使用 createRoot
,所有更新都将自动批处理,无论它们来自何处。
这意味着timeout、promise、原生事件处理程序或任何其他事件内部的更新将以与 React 事件内部的更新相同的方式进行批处理。我们希望这会减少渲染工作,从而提高应用程序的性能:
1function App() { 2 const [count, setCount] = useState(0); 3 const [flag, setFlag] = useState(false); 4 5 function handleClick() { 6 fetchSomething().then(() => { 7 // React 18 and later DOES batch these: 8 setCount(c => c + 1); 9 setFlag(f => !f); 10 // React will only re-render once at the end (that's batching!) 11 }); 12 } 13 14 return ( 15 <div> 16 <button onClick={handleClick}>Next</button> 17 <h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1> 18 </div> 19 ); 20}
✅ Demo: React 18 with createRoot
batches even outside event handlers!(每次点击只会渲染一次)
🟡 Demo: React 18 with legacy render
keeps the old behavior(每次点击会渲染两次,如果使用之前的render
方法的话)
注意:作为采用 React 18 的一部分,预计您将升级到
createRoot
。render
的旧行为只是为了更轻松地使用这两个版本进行生产实验。
React 会自动批量更新,无论更新发生在哪里,所以:
1function handleClick() { 2 setCount(c => c + 1); 3 setFlag(f => !f); 4 // React will only re-render once at the end (that's batching!) 5}
行为与此相同:
1setTimeout(() => { 2 setCount(c => c + 1); 3 setFlag(f => !f); 4 // React will only re-render once at the end (that's batching!) 5}, 1000); 6
行为与此相同:
1fetch(/*...*/).then(() => { 2 setCount(c => c + 1); 3 setFlag(f => !f); 4 // React will only re-render once at the end (that's batching!) 5})
行为与此相同:
1elm.addEventListener('click', () => { 2 setCount(c => c + 1); 3 setFlag(f => !f); 4 // React will only re-render once at the end (that's batching!) 5});
注意:React 仅在通常安全的情况下才会批量更新。例如,React 确保对于每个用户主动的的事件(例如单击或按键),DOM 在下一个事件之前完全更新。例如,这可以确保禁用提交的表单不能提交两次。
如果我不想批处理怎么办?
通常,批处理是安全的,但某些代码可能依赖于在状态更改后立即从 DOM 中读取某些内容。对于这些用例,您可以使用 ReactDOM.flushSync()
选择退出批处理:
1import { flushSync } from 'react-dom'; // Note: react-dom, not react 2 3function handleClick() { 4 flushSync(() => { 5 setCounter(c => c + 1); 6 }); 7 // React has updated the DOM by now 8 flushSync(() => { 9 setFlag(f => !f); 10 }); 11 // React has updated the DOM by now 12}
我们预计这种情况不会很常见。
这对 Hooks 有什么影响吗?
如果您使用 Hooks,我们预计自动批处理在绝大多数情况下都能“正常工作”。 (如果没有请告诉我们!)
这对Class有什么影响吗?
请记住,React 事件处理程序期间的更新始终是批处理的,因此对于这些更新,没有任何更改。
在类组件的边缘情况下,这可能会成为一个问题。
类组件有一个实现怪癖,可以同步读取事件内部的状态更新。这意味着您可以在 setState
调用之间读取 this.state
:
1handleClick = () => { 2 setTimeout(() => { 3 this.setState(({ count }) => ({ count: count + 1 })); 4 5 // { count: 1, flag: false } 6 console.log(this.state); 7 8 this.setState(({ flag }) => ({ flag: !flag })); 9 }); 10};
在 React 18 中,情况不再如此。由于 setTimeout
中的所有更新都是批处理的,因此 React 不会同步渲染第一个 setState
的结果 - 渲染发生在下一个浏览器更新期间。所以渲染还没有发生:
1handleClick = () => { 2 setTimeout(() => { 3 this.setState(({ count }) => ({ count: count + 1 })); 4 5 // { count: 0, flag: false } 6 console.log(this.state); 7 8 this.setState(({ flag }) => ({ flag: !flag })); 9 }); 10};
请参阅 示意代码
如果这是升级到 React 18 的障碍,您可以使用 ReactDOM.flushSync
强制更新,但我们建议谨慎使用:
1handleClick = () => { 2 setTimeout(() => { 3 ReactDOM.flushSync(() => { 4 this.setState(({ count }) => ({ count: count + 1 })); 5 }); 6 7 // { count: 1, flag: false } 8 console.log(this.state); 9 10 this.setState(({ flag }) => ({ flag: !flag })); 11 }); 12};
请参阅 示意代码
此问题不会影响带有 Hooks 的函数组件,因为设置状态不会更新 useState
中的现有变量:
1function handleClick() { 2 setTimeout(() => { 3 console.log(count); // 0 4 setCount(c => c + 1); 5 setCount(c => c + 1); 6 setCount(c => c + 1); 7 console.log(count); // 0 8 }, 1000)
虽然当您采用 Hooks 时,这种行为可能会让您感到惊讶,但它为自动批处理铺平了道路。
unstable_batchedUpdates
怎么样?
一些 React 库使用这个未记录的 API 来强制对事件处理程序之外的 setState
进行批处理:
1import { unstable_batchedUpdates } from 'react-dom'; 2 3unstable_batchedUpdates(() => { 4 setCount(c => c + 1); 5 setFlag(f => !f); 6});
这个 API 在 18 中仍然存在,但不再需要了,因为批处理会自动发生。我们不会在 18 中删除它,尽管在流行的库不再依赖它的存在后,它可能会在未来的主要版本中删除。
公众号关注一波~
网站源码:linxiaowu66 · 豆米的博客
Follow:linxiaowu66 · Github
关于评论和留言
如果对本文 react18的批处理渲染(翻译) 的内容有疑问,请在下面的评论系统中留言,谢谢。