翎泠 發表於 2022-8-10 23:27:00

React重新渲染指南

<h2 id="前言">前言</h2>
<p>老早就想写一篇关于React渲染的文章,这两天看到一篇比较不错英文的文章,翻译一下(主要是谷歌翻译,手动狗头),文章底部会附上原文链接。</p>
<h2 id="介绍">介绍</h2>
<p>React 重新渲染的综合指南。该指南解释了什么是重新渲染,什么是必要的和不必要的重新渲染,什么情况下会触发 React 组件重新渲染。</p>
<p>还包括可以防止重新渲染重要的模式和一些导致不必要的重新渲染和性能不佳的反模式。每个模式和反模式都附有图片指引和工作代码示例。</p>
<h2 id="内容">内容</h2>
<h4 id="react-的重新渲染是什么">React 的重新渲染是什么?</h4>
<p>在谈论 React 性能时,我们需要关注两个主要阶段:</p>
<ul>
<li><strong>初始渲染</strong>- 当组件首次出现在屏幕上时发生</li>
<li><strong>重新渲染</strong>- 已经在屏幕上的组件的第二次和任何连续渲染</li>
</ul>
<p>当 React 需要使用一些新数据更新应用程序时,会发生重新渲染。通常,这是由于用户与应用程序交互或通过异步请求或某些订阅模型传入的一些外部数据而发生的。</p>
<p>没有任何异步数据更新的非交互式应用<strong>永远不会</strong>重新渲染,因此不需要关心重新渲染性能优化。</p>
<h4 id="必要和不必要的重新渲染是什么">必要和不必要的重新渲染是什么?</h4>
<p><strong>必要的重新渲染</strong>- 重新渲染作为更改源的组件,或直接使用新信息的组件。例如,如果用户在输入字段中键入内容,则管理其状态的组件需要在每次击键时更新自身,即重新渲染。</p>
<p><strong>不必要的重新渲染</strong>- 由于错误或低效的应用程序架构,应用程序通过不同的重新渲染机制导致组件的重新渲染。例如,如果用户在输入字段中键入,并且在每次击键时重新呈现整个页面,则该页面已被不必要地重新呈现。</p>
<p>不必要的重新渲染本身<strong>不是问题</strong>:React 非常快,通常能够在用户没有注意到任何事情的情况下处理它们。</p>
<p>但是,如果重新渲染发生得太频繁和/或在非常重的组件上发生,这可能会导致用户体验出现“滞后”,每次交互都会出现明显的延迟,甚至应用程序变得完全没有响应。</p>
<h4 id="react-组件什么时候会重新渲染自己">React 组件什么时候会重新渲染自己?</h4>
<p>组件自身重新渲染有四个原因:状态更改、父(或子)重新渲染、上下文更改和hooks更改。还有一个很大的误区:当组件的props发生变化时会发生重新渲染。就其本身而言,它是不正确的(参见下面的解释)。</p>
<h5 id="重新渲染的原因状态变化">重新渲染的原因:状态变化</h5>
<p>当组件的状态发生变化时,它会重新渲染自己。通常,它发生在回调或<code>useEffect</code>hooks中。</p>
<p>状态变化是所有重新渲染的“根”源。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7694013b12584ece98ba4723c8b127aa~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="重新渲染的原因父级重新渲染">重新渲染的原因:父级重新渲染</h5>
<p>如果父组件重新渲染,组件将重新渲染自己。或者,如果我们从相反的方向来看:当一个组件重新渲染时,它也会重新渲染它的所有子组件。</p>
<p>它总是从根向下渲染,子的重新渲染不会触发父的重新渲染。(这里有一些警告和边缘情况,请参阅完整指南了解更多详细信息:React Element、children、parents 和 re-renders 的奥秘)。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b095a37f66f84606aa469a66cf3c4872~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="重新渲染的原因context-变化">重新渲染的原因:context 变化</h5>
<p>当 Context Provider 中的值发生变化时,<strong>所有</strong>使用此 Context 的组件都将重新渲染,即使它们不直接使用数据变化的部分。这些重新渲染无法通过直接memo来防止,但是有一些可以模拟的变通方法</p>
<p>参见【第 7 部分:防止由 Context 引起的重新渲染】</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ad5be59debd747cbaa53be387a5ba8b6~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="重新渲染的原因hooks变化">重新渲染的原因:hooks变化</h5>
<p>hooks内发生的所有事情都“属于”使用它的组件。关于conext和状态变化的相同规则适用于此:</p>
<ul>
<li>hooks内的状态更改将触发不可避免的宿主重复渲染</li>
<li>如果hooks使用了 Context 并且 Context 的值发生了变化,它会触发一个不可避免的重复渲染</li>
</ul>
<p>hooks可以是链式的。链中的每个钩子仍然属于宿主,同样的规则适用于它们中的任何一个。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/17c1de568d3f422f84a07eca6f00a811~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="重新渲染的原因props的变化很大的一个误区">重新渲染的原因:props的变化(很大的一个误区)</h5>
<p>当谈到没有被memo包裹的组件重新渲染,组件的props是否改变并不重要。</p>
<p>为了改变 props,它们需要由父组件更新。这意味着父组件必须重新渲染,这将触发子组件的重新渲染,而不管props是什么。</p>
<p>只有当使用momo技术(<code>React.memo</code>,&nbsp;<code>useMemo</code>)时,props的变化才变得重要。</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d2376a59867f402fa7ff90e246724224~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h4 id="防止合成重复渲染">防止合成重复渲染?</h4>
<h5 id="反模式在渲染函数中创建组件">反模式:在渲染函数中创建组件</h5>
<p>在另一个组件的渲染函数中创建组件是一种反模式,可能是最大的性能杀手。在每次重新渲染时,React 都会重新安装这个组件(即销毁它并从头开始重新创建它),这将比正常的重新渲染慢得多。最重要的是,这将导致以下错误:</p>
<ul>
<li>重新渲染期间可能出现内容“闪烁”</li>
<li>每次重新渲染时都会在组件中重置状态</li>
<li>useEffect 每次重新渲染都不会触发依赖项</li>
<li>如果一个组件被聚焦,焦点将丢失</li>
</ul>
<p>需要阅读的其他资源:如何编写高性能的 React 代码:规则、模式、注意事项</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fdfe4d20a00f498da0738cddec9053c7~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="向下移动状态">向下移动状态</h5>
<p>当一个重量级组件管理状态,并且这个状态只用于呈现树的一小部分时,这种模式会很有用。一个典型的例子是在呈现页面大部分内容的复杂组件中通过单击按钮打开/关闭对话框。</p>
<p>在这种情况下,控制模态对话框外观的状态、对话框本身以及触发更新的按钮都可以封装在一个更小的组件中。因此,较大的组件不会在这些状态更改时重新渲染。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8b76c794d2ff4c3c90850dd327a2e89e~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="children-作为-props">children 作为 props</h5>
<p>这也可以称为“包裹状态作为children”。这种模式类似于“下移状态”:它将状态变化封装在一个较小的组件中。这里的区别在于状态用于包装渲染树的缓慢部分的元素,因此不能那么容易地使用它。一个典型的例子是附加到组件根元素的回调<code>onScroll</code>。<code>onMouseMove</code></p>
<p>在这种情况下,可以将状态管理和使用该状态的组件提取到一个较小的组件中,并将<code>VerySlowComponent</code>组件作为<code>children</code>.&nbsp;从较小的组件角度来看,子组件只是props,所以它们不会受到状态变化的影响,因此不会重新渲染。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a03c313ada7849889156c07d5d2610d2~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="组件作为props">组件作为props</h5>
<p>与之前的模式几乎相同,具有相同的行为:它将状态封装在一个较小的组件中,而重组件作为 props 传递给它。道具不受状态变化的影响,因此重型组件不会重新渲染。</p>
<p>当一些重量级组件独立于状态,但不能作为一个组作为子级提取时,它可能很有用。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c5e542f203b6482b9d2a744c4ff172de~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h4 id="使用-reactmemo-防止重新渲染">使用 React.memo 防止重新渲染</h4>
<p>在React中包装组件。Memo将停止在渲染树的下游重新渲染,除非这个组件的props发生了变化。</p>
<p>当渲染一个不依赖于重渲染源(例如,状态,更改的数据)的重渲染组件时,这是很有用的。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7dfbdbe4393d4a5e8cce9752678ae772~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="组件有-props">组件有 props</h5>
<p>所有不是原始值的props都必须被useMemo,以便 React.memo 工作</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/59c01a82e34e4de59c74d5303b173266~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="组件作为props或children">组件作为props或children</h5>
<p><code>React.memo</code>必须应用于作为children或props传递的元素。memo父组件将不工作:子组件和props将是对象,因此它们会随着每次重新渲染而改变。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/88f9fda0110a49a59585bcf199d4b438~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h4 id="使用-usememousecallback-提高重新渲染性能">使用 useMemo/useCallback 提高重新渲染性能</h4>
<h5 id="反模式props-上不必要的-usememousecallback">反模式:props 上不必要的 useMemo/useCallback</h5>
<p>记忆的props不会阻止子组件的重新渲染。如果父组件重新渲染,它将触发子组件的重新渲染,而不管其props如何。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a07bf353cf76455cab2f546edac8c33f~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="必要的-usememousecallback">必要的 useMemo/useCallback</h5>
<p>如果一个子组件被<code>React.memo</code>包裹,所有不是值类型的props都必须被记忆。</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/efb64bfc7f52497c8273fedf413d5e33~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<p><code>useEffect</code>如果在一个组件中, 之类的hooks中使用非值类型作为依赖项。<br>
则应该使用<code>useMemo</code>,<code>useCallback</code>对其进行记忆。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/25d0ce324ee9425e9b8b41d7b08c8f42~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="使用memo-进行昂贵的计算">使用Memo 进行昂贵的计算</h5>
<p>其中一个用例<code>useMemo</code>是避免每次重新渲染时进行昂贵的计算。</p>
<p><code>useMemo</code>有它的成本(消耗一些内存并使初始渲染稍微慢一些),所以它不应该用于每次计算。在 React 中,在大多数情况下,安装和更新组件将是最昂贵的计算(除非您实际上是在计算素数,否则无论如何都不应该在前端进行)。</p>
<p>因此,典型的用例<code>useMemo</code>是记忆 React 元素。通常是现有渲染树的一部分或生成的渲染树的结果,例如返回新元素的映射函数。</p>
<p>与组件更新相比,“纯”javascript 操作(如排序或过滤数组)的成本通常可以忽略不计。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e454b241f38645489779cae43882b464~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h4 id="提高列表的重新渲染性能">提高列表的重新渲染性能</h4>
<p>除了常规的重新渲染规则和模式之外,该<code>key</code>属性还会影响 React 中列表的性能。</p>
<p><strong>重要提示</strong>:仅提供<code>key</code>属性不会提高列表的性能。为了防止重新呈现列表元素,您需要将它们包装起来<code>React.memo</code>并遵循其所有最佳实践。</p>
<p>值作为<code>key</code>应该是一个字符串,这在列表中每个元素的重新渲染之间是一致的。通常,使用项目<code>id</code>&nbsp;或数组<code>index</code>。</p>
<p>可以使用数组<code>index</code>作为键,如果列表是<strong>静态</strong>的,即不添加/删除/插入/重新排序元素。</p>
<p>在动态列表上使用数组的索引会导致:</p>
<ul>
<li>如果项目具有状态或任何不受控制的元素(如表单输入),则会出现错误</li>
<li>如果项目包装在 React.memo 中,性能会下降</li>
</ul>
<p>在此处阅读有关此内容的更多详细信息:React 关键属性:性能列表的最佳实践。</p>
<p><img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/68eee9cbdb9e4804be6ce0169e4b7ff5~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="反模式随机值作为列表中的键">反模式:随机值作为列表中的键</h5>
<p>随机生成的值永远不应用作<code>key</code>列表中属性的值。它们将导致 React 在每次重新渲染时重新安装项目,这将导致:</p>
<ul>
<li>列表的表现很差</li>
<li>如果项目具有状态或任何不受控制的元素(如表单输入),则会出现错误</li>
</ul>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0d0bf890c8244334824658a0ff4aedaf~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h4 id="防止由context引起的重新渲染">防止由Context引起的重新渲染</h4>
<h5 id="记忆-provider-值">记忆 Provider 值</h5>
<p>如果 Context Provider 不是放在应用程序的最根目录,并且由于其祖先的更改,它可能会重新渲染自身,则应该记住它的值。</p>
<p><img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/31ea0416bd5c4679a233af22e4e67aae~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="拆分数据和-api">拆分数据和 API</h5>
<p>如果在 Context 中存在数据和 API(getter 和 setter)的组合,则它们可以拆分为同一组件下的不同 Provider。这样,使用 API 的组件仅在数据更改时不会重新渲染。</p>
<p>在此处阅读有关此模式的更多信息:如何使用 Context 编写高性能的 React 应用程序</p>
<p><img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6a17e44f4a6c4caf8b4c376108e31fd6~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h5 id="将数据分成块">将数据分成块</h5>
<p>如果 Context 管理一些独立的数据块,它们可以被拆分为同一个提供者下的更小的提供者。这样,只有更改块的消费者才会重新渲染。</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e8310be034904dca888b337d1bf3c793~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h4 id="context-selectors-上下文选择器">Context selectors (上下文选择器)</h4>
<p>使用部分 Context 值的没有办法阻止组件重新渲染,即使使用的数据没有更改,即使使用<code>useMemo</code>钩子也是如此。</p>
<p>然而,上下文选择器可以通过使用高阶组件和<code>React.memo</code>.</p>
<p>在此处阅读有关此模式的更多信息:React Hooks 时代的高阶组件</p>
<p><img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5cb89f980b84402ca23f446ed6b5d878~tplv-k3u1fbpfcp-watermark.image?" alt="image.png" loading="lazy"></p>
<h2 id="结束语">结束语</h2>
<p>已上的内容来自 # React re-renders guide: everything, all at once. 英文好的同学可以直接看原文更佳。</p>
<p>如果你觉得该文章不错,不妨</p>
<p>1、<strong>点赞</strong>,让更多的人也能看到这篇内容</p>
<p>2、<strong>关注我</strong>,让我们成为长期关系</p>
<p>3、关注公众号「<strong>前端有话说</strong>」,里面已有多篇原创文章,和开发工具,欢迎各位的关注,第一时间阅读我的文章</p><br><br>
来源:https://www.cnblogs.com/qiaojie/p/16574337.html
頁: [1]
查看完整版本: React重新渲染指南