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

来自菜鸟教程
跳转至:导航、​搜索

作为 Write for DOnations 计划的一部分,作者选择了 Creative Commons 来接受捐赠。

介绍

在 React 应用程序中,性能问题可能来自网络延迟、过度使用的 API、低效的第三方库,甚至是在遇到异常大的负载之前运行良好的结构良好的代码。 确定性能问题的根本原因可能很困难,但其中许多问题源于组件重新渲染。 组件重新渲染的次数超出预期,或者组件具有大量数据的操作,这可能导致每次渲染变慢。 因此,学习如何防止不必要的重新渲染有助于优化 React 应用程序的性能并为用户创造更好的体验。

在本教程中,您将专注于优化 React 组件的性能。 为了探索这个问题,您将构建一个组件来分析一段文本。 您将了解不同的操作如何触发重新渲染,以及如何使用 Hooks 和 memoization 来最小化昂贵的数据计算。 在本教程结束时,您将熟悉许多性能增强 Hook,例如 useMemo 和 useCallback Hook,以及需要它们的情况。

先决条件

第 1 步 - 使用 memo 防止重新渲染

在这一步中,您将构建一个分析 组件 的文本。 您将创建一个输入来获取一段文本和一个计算字母和符号频率的组件。 然后,您将创建一个文本分析器性能不佳的场景,并确定性能问题的根本原因。 最后,您将使用 React memo 函数来防止在父组件更改时重新渲染组件,但子组件的 props 不会更改。

在这一步结束时,您将拥有一个工作组件,您将在本教程的其余部分中使用该组件,并了解父重新渲染如何在子组件中产生性能问题。

构建文本分析器

首先,将 <textarea> 元素添加到 App.js。

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

nano src/components/App/App.js

然后添加一个带有 <label> 的 <textarea> 输入。 通过添加以下突出显示的代码,将标签放置在 <div> 中,其中 className 为 wrapper:

性能教程/src/components/App/App.js

import React from 'react';
import './App.css';

function App() {
  return(
    <div className="wrapper">
      <label htmlFor="text">
        <p>Add Your Text Here:</p>
        <textarea
          id="text"
          name="text"
          rows="10"
          cols="100"
        >
        </textarea>
      </label>
    </div>
  )
}

export default App;

这将为示例应用程序创建一个输入框。 保存并关闭文件。

打开App.css添加一些样式:

nano src/components/App/App.css

在 App.css 内部,将填充添加到 .wrapper 类,然后将 margin 添加到 div 元素。 将 CSS 替换为以下内容:

性能教程/src/components/App/App.css

.wrapper {
    padding: 20px;
}

.wrapper div {
    margin: 20px 0;
}

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

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

首先,制作目录:

mkdir src/components/CharacterMap

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

nano src/components/CharacterMap/CharacterMap.js

在内部,创建一个名为 CharacterMap 的组件,该组件将 text 作为道具并在 <div> 中显示 text:

性能教程/src/components/CharacterMap/CharacterMap.js

import React from 'react';
import PropTypes from 'prop-types';

export default function CharacterMap({ text }) {
  return(
    <div>
      Character Map:
      {text}
    </div>
  )
}

CharacterMap.propTypes = {
  text: PropTypes.string.isRequired
}

请注意,您正在为 text 属性添加 PropType 以引入一些弱类型。

添加一个函数来循环文本并提取字符信息。 将函数命名为 itemize 并将 text 作为参数传递。 itemize 函数将对每个字符进行多次循环,并且随着文本大小的增加会非常慢。 这将使它成为测试性能的好方法:

性能教程/src/components/CharacterMap/CharacterMap.js

import React from 'react';
import PropTypes from 'prop-types';

function itemize(text){
  const letters = text.split('')
    .filter(l => l !== ' ')
    .reduce((collection, item) => {
      const letter = item.toLowerCase();
      return {
        ...collection,
        [letter]: (collection[letter] || 0) + 1
      }
    }, {})
  return letters;
}

export default function CharacterMap({ text }) {
  return(
    <div>
      Character Map:
      {text}
    </div>
  )
}

CharacterMap.propTypes = {
  text: PropTypes.string.isRequired
}

在 itemize 中,通过对每个字符使用 .split 将文本转换为 array。 然后使用 .filter 方法 删除空格,并使用 .reduce 方法 遍历每个字母。 在 .reduce 方法中,使用 object 作为初始值,然后通过将字符转换为小写并将 1 添加到之前的总数或 0 如果没有以前的总数。 使用对象 扩展运算符 使用新值更新对象,同时保留以前的值。

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

性能教程/src/components/CharacterMap/CharacterMap.js

import React from 'react';
import PropTypes from 'prop-types';

function itemize(text){
  const letters = text.split('')
    .filter(l => l !== ' ')
    .reduce((collection, item) => {
      const letter = item.toLowerCase();
      return {
        ...collection,
        [letter]: (collection[letter] || 0) + 1
      }
    }, {})
  return Object.entries(letters)
    .sort((a, b) => b[1] - a[1]);
}

export default function CharacterMap({ text }) {
  return(
    <div>
      Character Map:
      {text}
    </div>
  )
}

CharacterMap.propTypes = {
  text: PropTypes.string.isRequired
}

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

性能教程/src/components/CharacterMap/CharacterMap.js

import React from 'react';
import PropTypes from 'prop-types';

function itemize(text){
  const letters = text.split('')
    .filter(l => l !== ' ')
    .reduce((collection, item) => {
      const letter = item.toLowerCase();
      return {
        ...collection,
        [letter]: (collection[letter] || 0) + 1
      }
    }, {})
  return Object.entries(letters)
    .sort((a, b) => b[1] - a[1]);
}

export default function CharacterMap({ text }) {
  return(
    <div>
      Character Map:
      {itemize(text).map(character => (
        <div key={character[0]}>
          {character[0]}: {character[1]}
        </div>
      ))}
    </div>
  )
}

CharacterMap.propTypes = {
  text: PropTypes.string.isRequired
}

保存并关闭文件。

现在导入组件并在 App.js 中渲染。 打开App.js:

nano src/components/App/App.js

在您可以使用该组件之前,您需要一种存储文本的方法。 导入 useState 然后调用函数并将值存储在一个名为 text 的变量和一个名为 setText 的更新函数中。

要更新 text ,请向 onChange 添加一个函数,将 event.target.value 传递给 setText 函数:

性能教程/src/components/App/App.js

import React, { useState } from 'react';
import './App.css';

function App() {
  const [text, setText] = useState('');

  return(
    <div className="wrapper">
      <label htmlFor="text">
        <p>Your Text</p>
        <textarea
          id="text"
          name="text"
          rows="10"
          cols="100"
          onChange={event => setText(event.target.value)}
        >
        </textarea>
      </label>
    </div>
  )
}

export default App;

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

导入 CharacterMap 并在 <label> 元素之后渲染它。 将 text 状态传递给 text 属性:

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { useState } from 'react';
import './App.css';

import CharacterMap from '../CharacterMap/CharacterMap';

function App() {
  const [text, setText] = useState('');

  return(
    <div className="wrapper">
      <label htmlFor="text">
        <p>Your Text</p>
        <textarea
          id="text"
          name="text"
          rows="10"
          cols="100"
          onChange={event => setText(event.target.value)}
        >
        </textarea>
      </label>
      <CharacterMap text={text} />
    </div>
  )
}

export default App;

保存文件。 当你这样做时,浏览器会刷新,当你添加文本时,你会发现输入后的字符分析:

如示例中所示,该组件在少量文本的情况下表现得相当好。 每次击键时,React 都会用新数据更新 CharacterMap。 但是本地性能可能会产生误导。 并非所有设备都具有与您的开发环境相同的内存。

测试性能

有多种方法可以测试应用程序的性能。 您可以添加大量文本,也可以将浏览器设置为使用更少的内存。 要将组件推向性能瓶颈,请复制 GNU 的 Wikipedia 条目并将其粘贴到文本框中。 根据维基百科页面的编辑方式,您的示例可能会略有不同。

将条目粘贴到文本框中后,尝试输入附加字母 e 并注意显示需要多长时间。 在字符映射更新之前会有一个明显的停顿:

如果组件不够慢,并且您正在使用 Firefox、Edge 或其他浏览器,请添加更多文本,直到您注意到速度变慢。

如果您使用的是 Chrome,您可以在性能选项卡中限制 CPU。 这是模拟智能手机或旧硬件的好方法。 有关更多信息,请查看 Chrome DevTools 文档。

如果组件在 Wikipedia 条目中太慢,请删除一些文本。 您希望收到明显的延迟,但又不想让它变得异常缓慢或使浏览器崩溃。

防止重新渲染子组件

itemize 函数是上一节中确定的延迟的根源。 该函数通过多次循环内容来对每个条目进行大量工作。 您可以直接在函数本身中执行一些优化,但本教程的重点是如何在文本未更改时处理组件重新渲染。 换言之,您会将 itemize 函数视为您无权更改的函数。 目标是仅在必要时运行它。 这将展示如何处理您无法控制的 API 或第三方库的性能。

首先,您将探索父组件更改但子组件未更改的情况。

在 App.js 内部,添加一段解释组件如何工作的段落和一个用于切换信息的按钮:

性能教程/src/components/App/App.js

import React, { useReducer, useState } from 'react';
import './App.css';

import CharacterMap from '../CharacterMap/CharacterMap';

function App() {
  const [text, setText] = useState('');
  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)

  return(
    <div className="wrapper">
      <label htmlFor="text">
        <p>Your Text</p>
        <textarea
          id="text"
          name="text"
          rows="10"
          cols="100"
          onChange={event => setText(event.target.value)}
        >
        </textarea>
      </label>
      <div>
        <button onClick={toggleExplanation}>Show Explanation</button>
      </div>
      {showExplanation &&
        <p>
          This displays a list of the most common characters.
        </p>
      }
      <CharacterMap text={text} />
    </div>
  )
}

export default App;

使用 reducer 函数调用 useReducer Hook 以反转当前状态。 将输出保存到 showExplanation 和 toggleExplanation。 在 <label> 之后,添加一个按钮来切换解释和一个在 showExplanation 为真时将呈现的段落。

保存并退出文件。 当浏览器刷新时,单击按钮以切换说明。 注意有延迟。

这提出了一个问题。 您的用户在切换少量 JSX 时不应遇到延迟。 出现延迟是因为当父组件发生变化时——在这种情况下为 App.js——CharacterMap 组件正在重新渲染和重新计算字符数据。 text 属性是相同的,但是组件仍然会重新渲染,因为 React 会在父级更改时重新渲染整个组件树。

如果您使用浏览器的开发人员工具对应用程序进行概要分析,您会发现组件会重新呈现,因为父级更改。 有关使用开发人员工具进行分析的回顾,请查看 如何使用 React 开发人员工具调试 React 组件。

由于 CharacterMap 包含一个昂贵的函数,它应该只在道具更改时重新渲染它。

打开CharacterMap.js:

nano src/components/CharacterMap/CharacterMap.js

接下来,导入 memo,然后将组件传递给 memo 并将结果导出为默认值:

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { memo } from 'react';
import PropTypes from 'prop-types';

function itemize(text){
  ...
}

function CharacterMap({ text }) {
  return(
    <div>
      Character Map:
      {itemize(text).map(character => (
        <div key={character[0]}>
          {character[0]}: {character[1]}
        </div>
      ))}
    </div>
  )
}

CharacterMap.propTypes = {
  text: PropTypes.string.isRequired
}

export default memo(CharacterMap);

保存文件。 当您这样做时,浏览器将重新加载,并且在您单击按钮后将不再有延迟,然后才能获得结果:

如果您查看开发人员工具,您会发现组件不再重新渲染:

memo 函数将对道具进行浅层比较,并且仅在道具更改时才会重新渲染。 浅比较将使用 === 运算符将前一个道具与当前道具进行比较。

重要的是要记住,比较不是免费的。 检查 props 是有性能成本的,但是当您有明显的性能影响(例如昂贵的计算)时,防止重新渲染是值得的。 此外,由于 React 执行的是浅比较,因此当 prop 是对象或函数时,组件仍会重新渲染。 您将在第 3 步中阅读有关将函数作为道具处理的更多信息。

在这一步中,您创建了一个计算时间长、速度慢的应用程序。 您了解了父组件重新渲染如何导致子组件重新渲染以及如何使用 memo 防止重新渲染。 在下一步中,您将记住组件中的操作,以便仅在特定属性更改时执行操作。

第 2 步 — 使用 useMemo 缓存昂贵的数据计算

在此步骤中,您将使用 useMemo Hook 存储慢速数据计算的结果。 然后,您将 useMemo Hook 合并到现有组件中,并设置数据重新计算的条件。 在这一步结束时,您将能够缓存昂贵的函数,以便它们仅在特定数据更改时运行。

在上一步中,组件的切换解释是父组件的一部分。 但是,您可以改为将其添加到 CharacterMap 组件本身。 当你这样做时,CharacterMap会有两个属性,text和showExplanation,当showExplanation为真时会显示解释。

首先,打开 CharacterMap.js:

nano src/components/CharacterMap/CharacterMap.js

在 CharacterMap 内部,添加 showExplanation 的新属性。 当showExplanation的值为真时显示说明文字:

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { memo } from 'react';
import PropTypes from 'prop-types';

function itemize(text){
  ...
}

function CharacterMap({ showExplanation, text }) {
  return(
    <div>
      {showExplanation &&
        <p>
          This display a list of the most common characters.
        </p>
      }
      Character Map:
      {itemize(text).map(character => (
        <div key={character[0]}>
          {character[0]}: {character[1]}
        </div>
      ))}
    </div>
  )
}

CharacterMap.propTypes = {
  showExplanation: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default memo(CharacterMap);

保存并关闭文件。

接下来,打开App.js:

nano src/components/App/App.js

删除解释段并将 showExplanation 作为道具传递给 CharacterMap:

性能教程/src/components/App/App.js

import React, { useReducer, useState } from 'react';
import './App.css';

import CharacterMap from '../CharacterMap/CharacterMap';

function App() {
  const [text, setText] = useState('');
  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)

  return(
    <div className="wrapper">
      <label htmlFor="text">
        <p>Your Text</p>
        <textarea
          id="text"
          name="text"
          rows="10"
          cols="100"
          onChange={event => setText(event.target.value)}
        >
        </textarea>
      </label>
      <div>
        <button onClick={toggleExplanation}>Show Explanation</button>
      </div>
      <CharacterMap showExplanation={showExplanation} text={text} />
    </div>
  )
}

export default App;

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

如果您查看分析器,您会发现组件重新渲染是因为 showExplanation 属性发生了变化:

memo 函数将比较 props 并在没有 props 更改时防止重新渲染,但在这种情况下 showExplanation prop 确实发生了变化,因此整个组件将重新渲染并且组件将重新运行 itemize 功能。

在这种情况下,您需要记住组件的特定部分,而不是整个组件。 React 提供了一个名为 useMemo 的特殊 Hook,您可以使用它在重新渲染时保留组件的某些部分。 Hook 有两个参数。 第一个参数是一个函数,它将返回您要记忆的值。 第二个参数是一个依赖数组。 如果依赖项发生变化,useMemo 将重新运行该函数并返回一个值。

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

nano src/components/CharacterMap/CharacterMap.js

声明一个名为 characters 的新变量。 然后调用 useMemo 并传递一个匿名函数,该函数返回 itemize(text) 的值作为第一个参数,并传递一个包含 text 作为第二个参数的数组。 当useMemo运行时,会将itemize(text)的结果返回给characters变量。

将 JSX 中对 itemize 的调用替换为 characters:

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';

function itemize(text){
  ...
}

function CharacterMap({ showExplanation, text }) {
  const characters = useMemo(() => itemize(text), [text]);
  return(
    <div>
      {showExplanation &&
        <p>
          This display a list of the most common characters.
        </p>
      }
      Character Map:
      {characters.map(character => (
        <div key={character[0]}>
          {character[0]}: {character[1]}
        </div>
      ))}
    </div>
  )
}

CharacterMap.propTypes = {
  showExplanation: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}

export default memo(CharacterMap);

保存文件。 当您这样做时,浏览器将重新加载,并且在您切换解释时不会有延迟。

如果你对组件进行分析,你仍然会发现它会重新渲染,但渲染所需的时间会短得多。 在此示例中,它花费了 0.7 毫秒,而没有 useMemo Hook 则为 916.4 毫秒。 那是因为 React 正在重新渲染组件,但它没有重新运行包含在 useMemo Hook 中的函数。 您可以保留结果,同时仍然允许组件的其他部分更新:

如果更改文本框中的文本,仍然会有延迟,因为依赖项——text——改变了,所以useMemo会重新运行函数。 如果它没有重新运行,您将拥有旧数据。 关键是它只在它需要的数据发生变化时运行。

在这一步中,您记住了组件的某些部分。 您将一个昂贵的函数与组件的其余部分隔离开来,并使用 useMemo Hook 仅在某些依赖项发生更改时运行该函数。 在下一步中,您将记住函数以防止浅比较重新呈现。

第 3 步 — 使用 useCallback 管理函数相等检查

在这一步中,您将处理在 JavaScript 中难以比较的道具。 当 props 改变时,React 使用严格的相等检查。 此检查确定何时重新运行 Hooks 以及何时重新渲染组件。 由于 JavaScript 函数和对象难以比较,因此在某些情况下,道具实际上是相同的,但仍会触发重新渲染。

您可以使用 useCallback Hook 来跨重新渲染保留函数。 当父组件重新创建函数时,这将防止不必要的重新渲染。 在这一步结束时,您将能够使用 useCallback Hook 防止重新渲染。

在构建 CharacterMap 组件时,您可能会遇到需要它更灵活的情况。 在 itemize 函数中,您总是将字符转换为小写,但组件的某些使用者可能不想要该功能。 他们可能想要比较大小写字符或想要将所有字符转换为大写。

为此,添加一个名为 transformer 的新道具,它将更改角色。 transformer 函数可以是任何将字符作为参数并返回某种字符串的函数。

在 CharacterMap 内部,添加 transformer 作为道具。 给它一个 PropType 的函数,默认为 null:

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';

function itemize(text){
  const letters = text.split('')
    .filter(l => l !== ' ')
    .reduce((collection, item) => {
      const letter = item.toLowerCase();
      return {
        ...collection,
        [letter]: (collection[letter] || 0) + 1
      }
    }, {})
  return Object.entries(letters)
    .sort((a, b) => b[1] - a[1]);
}

function CharacterMap({ showExplanation, text, transformer }) {
  const characters = useMemo(() => itemize(text), [text]);
  return(
    <div>
      {showExplanation &&
        <p>
          This display a list of the most common characters.
        </p>
      }
      Character Map:
      {characters.map(character => (
        <div key={character[0]}>
          {character[0]}: {character[1]}
        </div>
      ))}
    </div>
  )
}

CharacterMap.propTypes = {
  showExplanation: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired,
  transformer: PropTypes.func
}

CharacterMap.defaultProps = {
  transformer: null
}

export default memo(CharacterMap);

接下来,更新 itemize 以将 transformer 作为参数。 用变压器代替.toLowerCase方法。 如果 transformer 为真,则以 item 作为参数调用该函数。 否则,返回 item:

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';

function itemize(text, transformer){
  const letters = text.split('')
    .filter(l => l !== ' ')
    .reduce((collection, item) => {
      const letter = transformer ? transformer(item) : item;
      return {
        ...collection,
        [letter]: (collection[letter] || 0) + 1
      }
    }, {})
  return Object.entries(letters)
    .sort((a, b) => b[1] - a[1]);
}

function CharacterMap({ showExplanation, text, transformer }) {
    ...
}

CharacterMap.propTypes = {
  showExplanation: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired,
  transformer: PropTypes.func
}

CharacterMap.defaultProps = {
  transformer: null
}

export default memo(CharacterMap);

最后,更新 useMemo Hook。 添加 transformer 作为依赖项并将其传递给 itemize 函数。 您希望确保您的依赖项是详尽无遗的。 这意味着您需要添加任何可能更改为依赖项的内容。 如果用户通过在不同选项之间切换来更改 transformer,则需要重新运行该函数以获得正确的值。

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { memo, useMemo } from 'react';
import PropTypes from 'prop-types';

function itemize(text, transformer){
  ...
}

function CharacterMap({ showExplanation, text, transformer }) {
  const characters = useMemo(() => itemize(text, transformer), [text, transformer]);
  return(
    <div>
      {showExplanation &&
        <p>
          This display a list of the most common characters.
        </p>
      }
      Character Map:
      {characters.map(character => (
        <div key={character[0]}>
          {character[0]}: {character[1]}
        </div>
      ))}
    </div>
  )
}

CharacterMap.propTypes = {
  showExplanation: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired,
  transformer: PropTypes.func
}

CharacterMap.defaultProps = {
  transformer: null
}

export default memo(CharacterMap);

保存并关闭文件。

在此应用程序中,您不想让用户能够在不同功能之间切换。 但是您确实希望字符为小写。 在 App.js 中定义一个 transformer 将字符转换为小写。 这个函数永远不会改变,但你需要将它传递给 CharacterMap。

打开App.js:

nano src/components/App/App.js

然后定义一个名为 transformer 的函数,将字符转换为小写。 将函数作为道具传递给 CharacterMap:

性能教程/src/components/CharacterMap/CharacterMap.js

import React, { useReducer, useState } from 'react';
import './App.css';

import CharacterMap from '../CharacterMap/CharacterMap';

function App() {
  const [text, setText] = useState('');
  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
  const transformer = item => item.toLowerCase();

  return(
    <div className="wrapper">
      <label htmlFor="text">
        <p>Your Text</p>
        <textarea
          id="text"
          name="text"
          rows="10"
          cols="100"
          onChange={event => setText(event.target.value)}
        >
        </textarea>
      </label>
      <div>
        <button onClick={toggleExplanation}>Show Explanation</button>
      </div>
      <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} />
    </div>
  )
}

export default App;

保存文件。 当你这样做时,你会发现当你切换解释时延迟又回来了。

如果你 profile 组件,你会发现组件重新渲染是因为 props 改变了,Hooks 改变了:

如果你仔细观察,你会发现 showExplanation 属性发生了变化,这是有道理的,因为你点击了按钮,但是 transformer 属性也发生了变化。

当您通过单击按钮在 App 中进行状态更改时,App 组件会重新渲染并重新声明 transformer。 尽管功能相同,但它与前一个功能在引用上并不相同。 这意味着它与之前的功能并不完全相同。

如果您打开浏览器控制台并比较相同的功能,您会发现比较是错误的,如下代码块所示:

const a = () = {};
const b = () = {};

a === a
// This will evaluate to true

a === b
// This will evaluate to false

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

为了避免这个问题,React 提供了一个名为 useCallback 的 Hook。 Hook 类似于 useMemo:它将一个函数作为第一个参数,将一个依赖数组作为第二个参数。 不同之处在于 useCallback 返回的是函数而不是函数的结果。 与 useMemo 挂钩一样,除非依赖项发生更改,否则它不会重新创建函数。 这意味着 CharacterMap.js 中的 useMemo Hook 将比较相同的值,并且 Hook 不会重新运行。

在 App.js 内部,导入 useCallback 并将匿名函数作为第一个参数传递,并将一个空数组作为第二个参数传递。 你永远不希望 App 重新创建这个函数:

性能教程/src/components/App/App.js

import React, { useCallback, useReducer, useState } from 'react';
import './App.css';

import CharacterMap from '../CharacterMap/CharacterMap';

function App() {
  const [text, setText] = useState('');
  const [showExplanation, toggleExplanation] = useReducer(state => !state, false)
  const transformer = useCallback(item => item.toLowerCase(), []);

  return(
    <div className="wrapper">
      <label htmlFor="text">
        <p>Your Text</p>
        <textarea
          id="text"
          name="text"
          rows="10"
          cols="100"
          onChange={event => setText(event.target.value)}
        >
        </textarea>
      </label>
      <div>
        <button onClick={toggleExplanation}>Show Explanation</button>
      </div>
      <CharacterMap showExplanation={showExplanation} text={text} transformer={transformer} />
    </div>
  )
}

export default App;

保存并关闭文件。 当您这样做时,您将能够切换说明而无需重新运行该功能。

如果你分析组件,你会发现 Hook 不再运行:

在这个特定组件中,您实际上并不需要 useCallback Hook。 您可以在组件之外声明该函数,并且它永远不会重新渲染。 如果它们需要某种 prop 或有状态数据,您应该只在组件内部声明函数。 但是有时您需要根据内部状态或道具创建函数,在这种情况下,您可以使用 useCallback 钩子来最小化重新渲染。

在这一步中,您使用 useCallback 钩子在重新渲染中保留了函数。 您还了解了当与 Hook 中的 props 或依赖项进行比较时,这些函数将如何保持相等性。

结论

您现在拥有提高昂贵组件性能的工具。 您可以使用 memo、useMemo 和 useCallback 来避免代价高昂的组件重新渲染。 但是所有这些策略都包括它们自己的性能成本。 memo 将花费额外的工作来比较属性,并且 Hooks 将需要在每个渲染上运行额外的比较。 仅在项目中有明确需求时才使用这些工具,否则您可能会增加自己的延迟。

最后,并非所有性能问题都需要技术修复。 有时性能成本是不可避免的——例如缓慢的 API 或大数据转换——在这些情况下,您可以通过渲染加载组件、在异步函数运行时显示占位符或对用户进行其他增强来解决问题经验。

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