如何利用 memo、useMemo 和 useCallback 避免 React 中的性能陷阱

_ 作者选择了 Creative Commons以作为 Write for Donations计划的一部分接收捐款。

介绍

在[React] (https://reactjs.org/)应用中,性能问题可能来自网络懒惰,过度工作API,效率低下的第三方库,甚至结构完善的代码,可以工作到遇到异常大的负载. 查明业绩问题的根源可能很困难,但其中许多问题源于部分的重新移交。 要么组件重报次数超过预期,要么组件有数据繁多的操作,可以使每个渲染速度缓慢. 由于这个原因,学习如何防止不需要的重接,可以帮助优化你的回放应用程序的性能,为用户创造更好的体验.

在这个教程中,你会专注于优化React组件中的性能. 为了探究这个问题,你会构建一个组件来分析一组文本. 您将查看不同的动作如何触发重报,以及如何使用 [Hooks] (https://reactjs.org/docs/hooks-intro.html)和 [memoization] (https://reactjs.org/docs/hooks-faq.html# how-to-memoize-calculations)来将昂贵的数据计算最小化. 到这个教程结束时,你会熟悉许多增强Hooks的性能,例如"useMemo"和"useCallback". 虎克,还有需要他们的情况.

前提条件

步骤 1 – 用备忘录防止重现

在这个步骤中,你会构建一个文本分析组件. 你会创建一个输入来取一个块的文本和一个组件来计算字母和符号的频率. 然后,你将创造出一种情景,即文本分析器的性能不佳,并找出性能问题的根源. 最后,您会使用 React `memo' 函数来防止当父母改变时将组件重接,但孩子组件的 [道 (https://andsky.com/tech/tutorials/how-to-customize-react-components-with-props) 不改变.

到本步骤结束时,您将有一个工作组件,您将使用在教程的剩余时间,并了解家长重现如何在儿童组件中产生性能问题。

建立一个文本分析器

首先,在App.js中添加一个<textarea>元素。

在您选择的文本编辑器中打开App.js:

1nano src/components/App/App.js

然后添加一个<textarea>输入,包含一个<label>。 将标签放入一个<div>中,并添加以下突出代码为classNamewrapper:

 1[label performance-tutorial/src/components/App/App.js]
 2import React from 'react';
 3import './App.css';
 4
 5function App() {
 6  return(
 7    <div className="wrapper">
 8      <label htmlFor="text">
 9        <p>Add Your Text Here:</p>
10        <textarea
11          id="text"
12          name="text"
13          rows="10"
14          cols="100"
15        >
16        </textarea>
17      </label>
18    </div>
19  )
20}
21
22export default App;

这将为样本应用程序创建一个输入框. 保存和关闭文件。

打开App.css,添加一些风格:

1nano src/components/App/App.css

App.css中,向.wrapper类添加插件,然后向div元素添加一个边缘

1[label performance-tutorial/src/components/App/App.css]
2
3.wrapper {
4    padding: 20px;
5}
6
7.wrapper div {
8    margin: 20px 0;
9}

这将增加输入和数据显示之间的分离. 保存和关闭文件。

接下来,为CharacterMap组件创建一个目录,该组件将分析文本,计算每个字母和符号的频率,并显示结果。

首先,创建地址:

1mkdir src/components/CharacterMap

然后在文本编辑器中打开CharacterMap.js:

1nano src/components/CharacterMap/CharacterMap.js

内部,创建一个名为CharacterMap的组件,将文本作为支架,并显示文本在一个<div>中:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React from 'react';
 3import PropTypes from 'prop-types';
 4
 5export default function CharacterMap({ text }) {
 6  return(
 7    <div>
 8      Character Map:
 9      {text}
10    </div>
11  )
12}
13
14CharacterMap.propTypes = {
15  text: PropTypes.string.isRequired
16}

请注意,您正在添加一个 PropType文本 prop 引入一些弱的打字。

添加一个函数在文本上循环并提取字符信息. 命名函数itemize并将text作为一个论点。 itemize函数将在每个字符上循环几次,随着文本大小增加,它将非常慢。

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text){
 6  const letters = text.split('')
 7    .filter(l => l !== ' ')
 8    .reduce((collection, item) => {
 9      const letter = item.toLowerCase();
10      return {
11        ...collection,
12        [letter]: (collection[letter] || 0) + 1
13      }
14    }, {})
15  return letters;
16}
17
18export default function CharacterMap({ text }) {
19  return(
20    <div>
21      Character Map:
22      {text}
23    </div>
24  )
25}
26
27CharacterMap.propTypes = {
28  text: PropTypes.string.isRequired
29}

在)去除空格,并使用[`.reduc'方法(https://andsky.com/tech/tutorials/how-to-use-array-methods-in-javascript-iteration-methods# reduce()去去除每个字母的平面。 在 " .减少 " 方法内,使用一个[标 (https://andsky.com/tech/tutorials/understanding-objects-in-javascript)作为初始值,然后通过将其转换为小写来使字符正常化,如果以前没有总计,则在上一个总计中加上 " 1 " 或 " 0 " 。 以新值更新对象,同时使用对象扩展运算符保留先前的值.

现在你已经创建了一个对象,每个字符的计数,你需要按最高字符来排序它。 将对象转换为对象的数组使用 Object.entries。 数组中的第一个项目是字符,第二个项目是计数。 使用 .sort方法将最常见的字符放在顶部:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text){
 6  const letters = text.split('')
 7    .filter(l => l !== ' ')
 8    .reduce((collection, item) => {
 9      const letter = item.toLowerCase();
10      return {
11        ...collection,
12        [letter]: (collection[letter] || 0) + 1
13      }
14    }, {})
15  return Object.entries(letters)
16    .sort((a, b) => b[1] - a[1]);
17}
18
19export default function CharacterMap({ text }) {
20  return(
21    <div>
22      Character Map:
23      {text}
24    </div>
25  )
26}
27
28CharacterMap.propTypes = {
29  text: PropTypes.string.isRequired
30}

最后,用文本调用itemize函数并显示结果:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2
 3import React from 'react';
 4import PropTypes from 'prop-types';
 5
 6function itemize(text){
 7  const letters = text.split('')
 8    .filter(l => l !== ' ')
 9    .reduce((collection, item) => {
10      const letter = item.toLowerCase();
11      return {
12        ...collection,
13        [letter]: (collection[letter] || 0) + 1
14      }
15    }, {})
16  return Object.entries(letters)
17    .sort((a, b) => b[1] - a[1]);
18}
19
20export default function CharacterMap({ text }) {
21  return(
22    <div>
23      Character Map:
24      {itemize(text).map(character => (
25        <div key={character[0]}>
26          {character[0]}: {character[1]}
27        </div>
28      ))}
29    </div>
30  )
31}
32
33CharacterMap.propTypes = {
34  text: PropTypes.string.isRequired
35}

保存并关闭文件。

现在,导入组件并在App.js内部渲染。

1nano src/components/App/App.js

导入 useState然后调用函数并将值存储在名为 text 的变量和一个名为 setText 的更新函数上。

若要更新「文本」,請在「onChange」中添加一個函數,將「event.target.value」傳送到「setText」函數:

 1[label performance-tutorial/src/components/App/App.js]
 2import React, { useState } from 'react';
 3import './App.css';
 4
 5function App() {
 6  const [text, setText] = useState('');
 7
 8  return(
 9    <div className="wrapper">
10      <label htmlFor="text">
11        <p>Your Text</p>
12        <textarea
13          id="text"
14          name="text"
15          rows="10"
16          cols="100"
17          onChange={event => setText(event.target.value)}
18        >
19        </textarea>
20      </label>
21    </div>
22  )
23}
24
25export default App;

请注意,您正在用一个空串初始化useState,这将确保您传输到CharacterMap组件的值始终是字符串,即使用户尚未输入文本。

导入 CharacterMap 并在 <label> 元素之后返回它. 将 text 状态传送到 text prop:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { useState } from 'react';
 3import './App.css';
 4
 5import CharacterMap from '../CharacterMap/CharacterMap';
 6
 7function App() {
 8  const [text, setText] = useState('');
 9
10  return(
11    <div className="wrapper">
12      <label htmlFor="text">
13        <p>Your Text</p>
14        <textarea
15          id="text"
16          name="text"
17          rows="10"
18          cols="100"
19          onChange={event => setText(event.target.value)}
20        >
21        </textarea>
22      </label>
23      <CharacterMap text={text} />
24    </div>
25  )
26}
27
28export default App;

当您这样做时,浏览器将更新,当您添加文本时,您将在输入后找到字符分析:

Input with results below

正如示例中所示,该组件在少量的文本中表现相当好. 每次按键时,React 都会更新CharacterMap以新的数据。

性能测试

您可以添加大量的文本或设置您的浏览器以使用更少的内存。 要将组件推到性能瓶颈,请复制 Wikipedia entry for GNU并将其粘贴到文本框中。 根据维基百科页面编辑的方式,您的样本可能略有不同。

在插入文本框后,尝试输入额外的字母e,并注意显示需要多长时间。

Animation showing the delay when typing "e" into the application

如果组件不够慢,并且您正在使用 Firefox, Edge,或其他任何浏览器,添加更多文本,直到您注意到放缓。

如果你正在使用 Chrome,你可以 throttle CPU 内部的性能卡. 这是一个很好的方式来模拟智能手机或较旧的硬件。 有关更多信息,请参阅 Chrome DevTools 文档

Performance Throttling in Chrome DevTools

如果组件在维基百科文献中太慢,请删除一些文本,您希望收到明显的延迟,但您不希望使它变得不太慢或破坏您的浏览器。

防止儿童组件的重新渲染

逐项 ' 函数是上一节所查明的延迟的根源。 该功能通过在内容上循环多次,对每个条目做了大量的工作. 在函数本身中可以直接实现一些优化,但此教程的焦点是当文本不改变时如何处理组件重递. 换句话说,您将把逐项`功能视为无法改变的功能。 目标只是在必要时运行。 这将显示如何处理无法控制的API或第三方库的性能.

首先,你将探索一个父母改变的情况,但孩子的组成部分不会改变。

App.js中,添加一个解释组件如何工作的段落和一个按钮来切换信息:

 1[label performance-tutorial/src/components/App/App.js]
 2import React, { useReducer, useState } from 'react';
 3import './App.css';
 4
 5import CharacterMap from '../CharacterMap/CharacterMap';
 6
 7function App() {
 8  const [text, setText] = useState('');
 9  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
10
11  return(
12    <div className="wrapper">
13      <label htmlFor="text">
14        <p>Your Text</p>
15        <textarea
16          id="text"
17          name="text"
18          rows="10"
19          cols="100"
20          onChange={event => setText(event.target.value)}
21        >
22        </textarea>
23      </label>
24      <div>
25        <button onClick={toggleExplanation}>Show Explanation</button>
26      </div>
27      {showExplanation &&
28        <p>
29          This displays a list of the most common characters.
30        </p>
31      }
32      <CharacterMap text={text} />
33    </div>
34  )
35}
36
37export default App;

使用减速器函数拨打useReducer链接以逆转当前状态. 将输出保存为显示解释toggle解释。 在<标签>之后,添加一个按钮来切换解释和一个段落,当显示解释是真实的时,将显示结果。

保存和退出文件. 当浏览器更新时,点击按钮来转移解释. 注意是否有延迟。

Delay when toggling the Explanation

你的用户不应该遇到一个延迟,当他们正在转移一个小量的 JSX 时,延迟发生,因为当母组件更改时,在这种情况下,App.js组件正在重新渲染和重新计算字符数据。

如果您使用浏览器的开发工具对应用程序进行配置,您会发现该组件会重新渲染,因为其主体变更。 有关使用开发工具的配置文件的审查,请参阅 如何使用 React Developer Tools 调试对应组件

Parent component re-renders in browser developer tools

由于CharacterMap包含一个昂贵的函数,它只应该在修订程序发生更改时重新渲染。

打开CharacterMap.js:

1nano src/components/CharacterMap/CharacterMap.js

接下来,导入备忘录,然后将组件传输到备忘录,并将结果导出为默认:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { memo } from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text){
 6  ...
 7}
 8
 9function CharacterMap({ text }) {
10  return(
11    <div>
12      Character Map:
13      {itemize(text).map(character => (
14        <div key={character[0]}>
15          {character[0]}: {character[1]}
16        </div>
17      ))}
18    </div>
19  )
20}
21
22CharacterMap.propTypes = {
23  text: PropTypes.string.isRequired
24}
25
26export default memo(CharacterMap);

当你这样做时,浏览器将重新加载,在你点击按钮之前你得到结果后不再有延迟:

No delay when toggling the explanation in the test app

如果你看看开发人员的工具,你会发现该组件不再重现:

Component did not re-render

备忘录函数将对附件进行较浅的比较,并且只有在附件更改时才会重新进行。

重要的是要记住,比较不是免费的。检查补贴有性能成本,但当你有一个清晰的性能影响,如昂贵的计算,它是值得防止再渲染的。

在此步骤中,您创建了一个应用程序,进行长而缓慢的计算。您了解了家长重现将如何导致儿童组件重现,以及如何使用备忘录防止重现。

步骤 2 – 使用useMemo缓存昂贵的数据计算

在此步骤中,您将使用useMemo夹存储慢数据计算的结果,然后将useMemo夹嵌入到现有组件中,并为数据重新计算设置条件。

在之前的步骤中,该组件的转移解释是母组件的一部分,但是,您可以将其添加到CharacterMap组件本身。当您这样做时,CharacterMap将具有两个属性,即文本显示解释,并且当显示解释是真实时,它将显示解释。

要开始,打开CharacterMap.js:

1nano src/components/CharacterMap/CharacterMap.js

CharacterMap 中,添加一个新的 showExplanation 属性. 当 showExplanation 的值为 true 时,显示说明文本:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { memo } from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text){
 6  ...
 7}
 8
 9function CharacterMap({ showExplanation, text }) {
10  return(
11    <div>
12      {showExplanation &&
13        <p>
14          This display a list of the most common characters.
15        </p>
16      }
17      Character Map:
18      {itemize(text).map(character => (
19        <div key={character[0]}>
20          {character[0]}: {character[1]}
21        </div>
22      ))}
23    </div>
24  )
25}
26
27CharacterMap.propTypes = {
28  showExplanation: PropTypes.bool.isRequired,
29  text: PropTypes.string.isRequired
30}
31
32export default memo(CharacterMap);

保存并关闭文件。

接下来,打开App.js:

1nano src/components/App/App.js

删除解释的段落,并将显示解释作为标语符号转移到CharakterMap:

 1[label performance-tutorial/src/components/App/App.js]
 2import React, { useReducer, useState } from 'react';
 3import './App.css';
 4
 5import CharacterMap from '../CharacterMap/CharacterMap';
 6
 7function App() {
 8  const [text, setText] = useState('');
 9  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
10
11  return(
12    <div className="wrapper">
13      <label htmlFor="text">
14        <p>Your Text</p>
15        <textarea
16          id="text"
17          name="text"
18          rows="10"
19          cols="100"
20          onChange={event => setText(event.target.value)}
21        >
22        </textarea>
23      </label>
24      <div>
25        <button onClick={toggleExplanation}>Show Explanation</button>
26      </div>
27      <CharacterMap showExplanation={showExplanation} text={text} />
28    </div>
29  )
30}
31
32export default App;

保存并关闭文件. 当您这样做时,浏览器将更新. 如果您切换解释,您将再次收到延迟。

Delay when toggling explanation

如果你看看配置文件,你会发现该组件重新渲染,因为显示解释字符串改变了:

Re-render because prop changed

备忘录函数将比较附件并防止再渲染,如果没有附件更改,但在这种情况下,显示解释附件不会更改,因此整个组件将重新渲染,组件将重新运行itemize函数。

在这种情况下,您需要记忆某些部分的组件,而不是整个组件。 React 提供了一个名为useMemo的特殊,您可以使用它来保存部分的组件在重现器中。 需要两个参数。 第一个参数是一个函数,将返回您想记忆的值。 第二个参数是依赖的组合。 如果依赖改变,则useMemo将重新运行该函数并返回一个值。

要实现useMemo,先打开CharacterMap.js:

1nano src/components/CharacterMap/CharacterMap.js

声明一个名为)的值作为第一个参数和包含text`的数组作为第二个参数。

在 JSX 中,用字符取代对itemize的呼叫:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { memo, useMemo } from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text){
 6  ...
 7}
 8
 9function CharacterMap({ showExplanation, text }) {
10  const characters = useMemo(() => itemize(text), [text]);
11  return(
12    <div>
13      {showExplanation &&
14        <p>
15          This display a list of the most common characters.
16        </p>
17      }
18      Character Map:
19      {characters.map(character => (
20        <div key={character[0]}>
21          {character[0]}: {character[1]}
22        </div>
23      ))}
24    </div>
25  )
26}
27
28CharacterMap.propTypes = {
29  showExplanation: PropTypes.bool.isRequired,
30  text: PropTypes.string.isRequired
31}
32
33export default memo(CharacterMap);

当你这样做时,浏览器将重新加载,当你切换解释时不会有延迟。

Animation showing no delay when toggling explanation

如果您配置组件,您仍然会发现它重新渲染,但渲染所需的时间会更短。在本示例中,它花费了.7 毫秒,而没有使用Memo Hook 就花费了 916.4 毫秒。 这是因为 React 正在重新渲染组件,但它没有重新运行使用Memo Hook 中包含的函数。

Developer tools profile showing that the component renders in .7ms

如果您在文本框中更改文本,仍会出现延迟,因为依赖性文本发生了变化,因此useMemo将重新运行该函数。

在此步骤中,您将组件的部分备忘录,您将昂贵的函数与其它组件隔离,并使用useMemo链接只在某些依赖性发生变化时运行该函数。

步骤 3 – 使用useCallback管理函数平等性检查

在此步骤中,您将处理在JavaScript中难以比较的代理程序。React使用严格的平等检查,当代理程序发生更改时。这个检查决定了何时重新运行Hooks以及何时重新渲染组件。

您可以使用 useCallback Hook 来保留一个函数在重新渲染器上。这将防止当一个母组件重现一个函数时出现不必要的重新渲染。

当你构建你的CharacterMap组件时,你可能会遇到一个情况,你需要它更灵活。在itemize函数中,你总是将字符转换为较低的案例,但某些组件的消费者可能不希望这种功能。

为了方便这一点,添加一个名为变压器的新支撑,这将改变字符. 变压器函数将是任何将一个字符作为论点并返回某种类型的字符串的函数。

CharacterMap中,将transformer添加为 prop. 给它一个PropType函数,默认值为null:

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { memo, useMemo } from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text){
 6  const letters = text.split('')
 7    .filter(l => l !== ' ')
 8    .reduce((collection, item) => {
 9      const letter = item.toLowerCase();
10      return {
11        ...collection,
12        [letter]: (collection[letter] || 0) + 1
13      }
14    }, {})
15  return Object.entries(letters)
16    .sort((a, b) => b[1] - a[1]);
17}
18
19function CharacterMap({ showExplanation, text, transformer }) {
20  const characters = useMemo(() => itemize(text), [text]);
21  return(
22    <div>
23      {showExplanation &&
24        <p>
25          This display a list of the most common characters.
26        </p>
27      }
28      Character Map:
29      {characters.map(character => (
30        <div key={character[0]}>
31          {character[0]}: {character[1]}
32        </div>
33      ))}
34    </div>
35  )
36}
37
38CharacterMap.propTypes = {
39  showExplanation: PropTypes.bool.isRequired,
40  text: PropTypes.string.isRequired,
41  transformer: PropTypes.func
42}
43
44CharacterMap.defaultProps = {
45  transformer: null
46}
47
48export default memo(CharacterMap);

接下来,更新「itemize」以将「transformer」作为参数。 将「.toLowerCase」方法替换为「transformer」。 如果「transformer」是真,请将具有「element」的函数称为参数。

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { memo, useMemo } from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text, transformer){
 6  const letters = text.split('')
 7    .filter(l => l !== ' ')
 8    .reduce((collection, item) => {
 9      const letter = transformer ? transformer(item) : item;
10      return {
11        ...collection,
12        [letter]: (collection[letter] || 0) + 1
13      }
14    }, {})
15  return Object.entries(letters)
16    .sort((a, b) => b[1] - a[1]);
17}
18
19function CharacterMap({ showExplanation, text, transformer }) {
20    ...
21}
22
23CharacterMap.propTypes = {
24  showExplanation: PropTypes.bool.isRequired,
25  text: PropTypes.string.isRequired,
26  transformer: PropTypes.func
27}
28
29CharacterMap.defaultProps = {
30  transformer: null
31}
32
33export default memo(CharacterMap);

最后,更新useMemo链接。添加transformer作为依赖,并将其传输到itemize函数中。你想确保你的依赖是完整的。这意味着你需要添加任何可能变更为依赖的东西。如果用户通过在不同的选项之间切换transformer,你需要重新运行该函数以获得正确的值。

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { memo, useMemo } from 'react';
 3import PropTypes from 'prop-types';
 4
 5function itemize(text, transformer){
 6  ...
 7}
 8
 9function CharacterMap({ showExplanation, text, transformer }) {
10  const characters = useMemo(() => itemize(text, transformer), [text, transformer]);
11  return(
12    <div>
13      {showExplanation &&
14        <p>
15          This display a list of the most common characters.
16        </p>
17      }
18      Character Map:
19      {characters.map(character => (
20        <div key={character[0]}>
21          {character[0]}: {character[1]}
22        </div>
23      ))}
24    </div>
25  )
26}
27
28CharacterMap.propTypes = {
29  showExplanation: PropTypes.bool.isRequired,
30  text: PropTypes.string.isRequired,
31  transformer: PropTypes.func
32}
33
34CharacterMap.defaultProps = {
35  transformer: null
36}
37
38export default memo(CharacterMap);

保存并关闭文件。

在这个应用程序中,你不想让用户能够在不同的函数之间切换,但你确实希望字符是较低的案例。在App.js中定义一个转换器,将字符转换为较低的案例。

打开App.js:

1nano src/components/App/App.js

然后定义一个名为transformer的函数,该函数将一个字符转换为较低的例子。

 1[label performance-tutorial/src/components/CharacterMap/CharacterMap.js]
 2import React, { useReducer, useState } from 'react';
 3import './App.css';
 4
 5import CharacterMap from '../CharacterMap/CharacterMap';
 6
 7function App() {
 8  const [text, setText] = useState('');
 9  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
10  const transformer = item => item.toLowerCase();
11
12  return(
13    <div className="wrapper">
14      <label htmlFor="text">
15        <p>Your Text</p>
16        <textarea
17          id="text"
18          name="text"
19          rows="10"
20          cols="100"
21          onChange={event => setText(event.target.value)}
22        >
23        </textarea>
24      </label>
25      <div>
26        <button onClick={toggleExplanation}>Show Explanation</button>
27      </div>
28      <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} />
29    </div>
30  )
31}
32
33export default App;

保存文件. 当您这样做时,当您切换解释时,您会发现延迟已经返回。

Animation showing a delay when toggling explanation

如果您对组件进行配置,则会发现组件会重新渲染,因为附件的变更和胡克的变更:

Profile for transformer

如果你仔细观察,你会发现显示解释插件改变了,这是有意义的,因为你点击了按钮,但变压器插件也改变了。

当您点击按钮在应用程序中进行状态更改时,应用程序组件重新渲染并重新声明转换器。尽管该函数是相同的,但它不是参考性相同的,这意味着它与之前的函数不完全相同。

如果你打开浏览器控制台并比较相同的函数,你会发现比较是假的,如下列代码块所示:

1const a = () = {};
2const b = () = {};
3
4a === a
5// This will evaluate to true
6
7a === b
8// This will evaluate to false

使用===比较运算符,此代码显示两个函数不被认为是平等的,即使它们具有相同的值。

为了避免这种问题,React 提供一个名为useCallback的链接。链接类似于useMemo:它将一个函数作为第一个参数和一系列依赖作为第二个参数。区别在于useCallback返回函数而不是函数的结果。

App.js中,导入useCallback,将匿名函数作为第一个参数和空数列作为第二个参数。

 1[label performance-tutorial/src/components/App/App.js]
 2import React, { useCallback, useReducer, useState } from 'react';
 3import './App.css';
 4
 5import CharacterMap from '../CharacterMap/CharacterMap';
 6
 7function App() {
 8  const [text, setText] = useState('');
 9  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
10  const transformer = useCallback(item => item.toLowerCase(), []);
11
12  return(
13    <div className="wrapper">
14      <label htmlFor="text">
15        <p>Your Text</p>
16        <textarea
17          id="text"
18          name="text"
19          rows="10"
20          cols="100"
21          onChange={event => setText(event.target.value)}
22        >
23        </textarea>
24      </label>
25      <div>
26        <button onClick={toggleExplanation}>Show Explanation</button>
27      </div>
28      <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} />
29    </div>
30  )
31}
32
33export default App;

保存和关闭文件. 当您这样做时,您将能够转换解释,而无需重新运行该函数。

Animation showing no delay when toggling the explanation component

如果你配置组件,你会发现不再运行:

Image of the browser developer tools showing that the Hook does not run

在这个特定的组件中,你实际上不需要useCallback链接,你可以声明该函数在组件外部,它永远不会重新渲染,你只应该声明组件内部的函数,如果它们需要某种类型的支持或状态数据,但有时你需要基于内部状态或支持来创建函数,在这些情况下,你可以使用使用Callback链接来最大限度地减少重新渲染。

在此步骤中,您使用useCallback链接保存了跨重返器的函数,您还了解了这些函数在链接中相比时如何保持平等性。

结论

现在你有工具来提高昂贵的组件的性能。你可以使用memo,useMemouseCallback,以避免昂贵的组件重现。但所有这些策略都包括自己的性能成本。

最后,并非所有性能问题都需要技术修复,有时性能成本是不可避免的,例如缓慢的API或大数据转换,在这些情况下,您可以使用设计来解决问题,无论是渲染加载组件,在运行非同步函数时显示位置,还是其他改进用户体验。

如果您想阅读更多 React 教程,请查看我们的 React 主题页面,或返回 如何编码在 React.js 系列页面

Published At
Categories with 技术
comments powered by Disqus