react18的批处理渲染(翻译)

发表于 2024-06-03
更新于 2024-06-03
分类于 技术专栏
阅读量 117
字数统计 6216

翻译自:https://github.com/reactwg/react-18/discussions/21

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 中删除它,尽管在流行的库不再依赖它的存在后,它可能会在未来的主要版本中删除。

公众号关注一波~

微信公众号

关于评论和留言

如果对本文 react18的批处理渲染(翻译) 的内容有疑问,请在下面的评论系统中留言,谢谢。

网站源码:linxiaowu66 · 豆米的博客

Follow:linxiaowu66 · Github