首页 >web前端 >js教程 >如何在REACT中实施回忆以提高性能

如何在REACT中实施回忆以提高性能

Joseph Gordon-Levitt
Joseph Gordon-Levitt原创
2025-02-09 09:00:15906浏览

How to Implement Memoization in React to Improve Performance

本教程将讲解如何在React中实现记忆化(Memoization)。记忆化通过存储昂贵函数调用的结果并在再次需要时返回这些缓存的结果来提高性能。

我们将涵盖以下内容:

  • React如何渲染UI
  • 为什么需要React记忆化
  • 如何为函数式和类组件实现记忆化
  • 关于记忆化的注意事项

本文假设您已经基本了解React中的类组件和函数式组件。如果您想复习这些主题,请查看官方React组件和props文档。

How to Implement Memoization in React to Improve Performance

关键要点

  • React中的记忆化通过存储昂贵函数调用的结果并在再次需要时返回这些缓存的结果来提高性能。
  • React使用虚拟DOM来高效地执行DOM更新,但是对于大型组件,检查虚拟DOM的性能影响可能很大。记忆化可以帮助避免不必要的重新渲染和虚拟DOM检查。
  • React.PureComponentReact.memo()分别可用于在类组件和函数式组件中实现记忆化。如果组件的props或state没有改变,这些方法可以防止不必要的重新渲染。
  • 如果一个函数作为prop传递给子组件,即使使用了React.memo(),子组件也会重新渲染。为了避免这种情况,可以使用useCallback()钩子来防止每次父组件渲染时都重新创建函数。
  • 应该谨慎地在React应用程序中使用记忆化。当组件对相同的props返回相同的输出、包含多个UI元素(虚拟DOM检查会影响性能)或经常提供相同的props时,它最有效。

React如何渲染UI

在详细介绍React中的记忆化之前,让我们首先看看React如何使用虚拟DOM渲染UI。

常规DOM基本上包含一组表示为树的节点。DOM中的每个节点都是UI元素的表示。每当应用程序中发生状态更改时,该UI元素及其所有子元素的相应节点都会在DOM中更新,然后重新绘制UI以反映更新的更改。

借助高效的树算法,更新节点的速度更快,但重新绘制速度较慢,并且当该DOM具有大量UI元素时,会对性能产生影响。因此,在React中引入了虚拟DOM。

这是真实DOM的虚拟表示。现在,每当应用程序的状态发生任何变化时,React不会直接更新真实DOM,而是创建一个新的虚拟DOM。然后,React将这个新的虚拟DOM与先前创建的虚拟DOM进行比较,以查找需要重新绘制的差异。

使用这些差异,虚拟DOM将有效地用更改更新真实的DOM。这提高了性能,因为虚拟DOM不会简单地更新UI元素及其所有子元素,而是有效地仅更新真实DOM中必要的最小更改。

为什么我们需要React记忆化

在上一节中,我们看到了React如何使用虚拟DOM有效地执行DOM更新以提高性能。在本节中,我们将查看一个用例,该用例解释了为了进一步提高性能而需要记忆化的原因。

我们将创建一个父类,其中包含一个按钮来递增名为count的状态变量。父组件还调用子组件,并将prop传递给它。我们还在两个类的render方法中添加了console.log()语句:

<code class="language-javascript">//Parent.js
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
  };

  render() {
    console.log("Parent render");
    return (
      <div classname="App">
        <button onclick="{this.handleClick}">Increment</button>
        <h2>Count: {this.state.count}</h2>
        <child name='{"joe"}'></child>
      </div>
    );
  }
}

export default Parent;</code>

此示例的完整代码可在CodeSandbox上找到。

我们将创建一个Child类,该类接受父组件传递的prop并在UI中显示它:

<code class="language-javascript">//Child.js
class Child extends React.Component {
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>Name: {this.props.name}</h2>
      </div>
    );
  }
}

export default Child;</code>

每当我们单击父组件中的按钮时,count值都会更改。由于这是一个状态更改,因此会调用父组件的render方法。

传递给子类的props在每次父级重新渲染时都保持不变,因此子组件不应重新渲染。但是,当我们运行上述代码并继续递增count时,我们会得到以下输出:

<code>Parent render
Child render
Parent render
Child render
Parent render
Child render</code>

您可以在以下sandbox中自己递增上述示例的count,并查看控制台的输出:

[此处应嵌入CodeSandbox链接,但由于我无法访问外部网站,无法提供]

从这个输出中,我们可以看到,当父组件重新渲染时,它也会重新渲染子组件——即使传递给子组件的props没有改变。这将导致子组件的虚拟DOM与之前的虚拟DOM执行差异检查。由于子组件中没有差异——因为props在所有重新渲染中都是相同的——因此不会更新真实的DOM。

我们确实具有不会不必要地更新真实DOM的性能优势,但我们可以在这里看到,即使子组件没有实际更改,也会创建新的虚拟DOM并执行差异检查。对于小型React组件,此性能可以忽略不计,但对于大型组件,性能影响很大。为了避免这种重新渲染和虚拟DOM检查,我们使用记忆化。

React中的记忆化

在React应用程序的上下文中,记忆化是一种技术,其中,每当父组件重新渲染时,只有当props发生更改时,子组件才会重新渲染。如果props没有更改,它不会执行render方法,而是返回缓存的结果。由于render方法未执行,因此不会创建虚拟DOM和差异检查——从而提高了性能。

现在,让我们看看如何在类和函数式React组件中实现记忆化以避免这种不必要的重新渲染。

(以下内容与原文类似,只是对语言和表达方式进行了一些调整,并保持了图片位置和格式不变。由于无法访问外部网站,我无法提供CodeSandbox链接。)

实现类组件中的记忆化

要在类组件中实现记忆化,我们将使用React.PureComponentReact.PureComponent实现了shouldComponentUpdate(),它对state和props进行浅比较,并且只有在props或state发生更改时才渲染React组件。

将子组件更改为如下所示的代码:

<code class="language-javascript">//Parent.js
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
  };

  render() {
    console.log("Parent render");
    return (
      <div classname="App">
        <button onclick="{this.handleClick}">Increment</button>
        <h2>Count: {this.state.count}</h2>
        <child name='{"joe"}'></child>
      </div>
    );
  }
}

export default Parent;</code>

此示例的完整代码如下所示:[此处应嵌入CodeSandbox链接]

父组件保持不变。现在,当我们在父组件中递增count时,控制台中的输出如下:

<code class="language-javascript">//Child.js
class Child extends React.Component {
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>Name: {this.props.name}</h2>
      </div>
    );
  }
}

export default Child;</code>

对于第一次渲染,它会调用父组件和子组件的render方法。

在每次递增的后续重新渲染中,只调用父组件的render函数。子组件不会重新渲染。

实现函数式组件中的记忆化

要在函数式React组件中实现记忆化,我们将使用React.memo()React.memo()是一个高阶组件(HOC),它与PureComponent执行类似的工作,避免不必要的重新渲染。

以下是函数式组件的代码:

<code>Parent render
Child render
Parent render
Child render
Parent render
Child render</code>

我们还将父组件转换为函数式组件,如下所示:

<code class="language-javascript">//Child.js
class Child extends React.PureComponent { // 将React.Component更改为React.PureComponent
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>Name: {this.props.name}</h2>
      </div>
    );
  }
}

export default Child;</code>

此示例的完整代码可在以下sandbox中看到:[此处应嵌入CodeSandbox链接]

现在,当我们在父组件中递增count时,控制台会输出以下内容:

<code>Parent render
Child render
Parent render
Parent render</code>

React.memo()对函数prop的问题

在上面的示例中,我们看到当我们对子组件使用React.memo() HOC时,即使父组件也重新渲染,子组件也不会重新渲染。

但是,需要注意的一个小问题是,如果我们将函数作为prop传递给子组件,即使使用了React.memo(),子组件也会重新渲染。让我们来看一个例子。

我们将更改父组件,如下所示。在这里,我们添加了一个处理程序函数,我们将其作为props传递给子组件:

<code class="language-javascript">//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>Name: {props.name}</h2>
    </div>
  );
}

export default React.memo(Child); // 在此处为子组件添加HOC以进行记忆化</code>

子组件代码保持不变。我们没有使用作为props传递的函数在子组件中:

<code class="language-javascript">//Parent.js
import React, { useState } from 'react';
import Child from './Child';

export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  console.log("Parent render");
  return (
    <div>
      <button onclick="{handleClick}">Increment</button>
      <h2>Count: {count}</h2>
      <child name='{"joe"}'></child>
    </div>
  );
}</code>

现在,当我们在父组件中递增count时,它会重新渲染并重新渲染子组件,即使props没有更改。

那么,是什么导致子组件重新渲染的呢?答案是,每次父组件重新渲染时,都会创建一个新的处理程序函数并将其传递给子组件。现在,由于处理程序函数在每次重新渲染时都会重新创建,因此子组件在对props进行浅比较时会发现处理程序引用已更改,并重新渲染子组件。

在下一节中,我们将看到如何解决此问题。

useCallback()以避免进一步重新渲染

导致子组件重新渲染的主要问题是处理程序函数的重新创建,这改变了传递给子组件的引用。因此,我们需要一种方法来避免这种重新创建。如果处理程序没有重新创建,则对处理程序的引用不会更改——因此子组件不会重新渲染。

为了避免每次父组件渲染时都重新创建函数,我们将使用一个名为useCallback()的React钩子。钩子是在React 16中引入的。要了解有关钩子的更多信息,您可以查看React的官方钩子文档,或查看“React Hooks:如何入门和构建您自己的”。

useCallback()钩子接受两个参数:回调函数和依赖项列表。

考虑以下useCallback()示例:

<code class="language-javascript">//Parent.js
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
  };

  render() {
    console.log("Parent render");
    return (
      <div classname="App">
        <button onclick="{this.handleClick}">Increment</button>
        <h2>Count: {this.state.count}</h2>
        <child name='{"joe"}'></child>
      </div>
    );
  }
}

export default Parent;</code>

在这里,useCallback()被添加到handleClick()函数中。第二个参数[x,y]可以是一个空数组、一个依赖项或一个依赖项列表。每当第二个参数中提到的任何依赖项发生更改时,才会重新创建handleClick()函数。

如果useCallback()中提到的依赖项没有更改,则返回回调函数(作为第一个参数)的记忆化版本。我们将更改我们的父函数式组件以对传递给子组件的处理程序使用useCallback()钩子:

(此处与原文类似,只是对语言和表达方式进行了一些调整,并保持了图片位置和格式不变。由于无法访问外部网站,我无法提供CodeSandbox链接。)

注意事项

记忆化是通过避免在组件的props或state没有更改时不必要地重新渲染组件来提高React应用程序性能的好技术。您可能会想到只为所有组件添加记忆化,但这并不是构建React组件的好方法。您应该仅在组件满足以下条件时才使用记忆化:

  • 给定相同的props时返回相同的输出
  • 具有多个UI元素,并且虚拟DOM检查会影响性能
  • 经常提供相同的props

总结

在本教程中,我们已经看到了:

  • React如何渲染UI
  • 为什么需要记忆化
  • 如何通过React.memo()为函数式React组件和React.PureComponent为类组件在React中实现记忆化
  • 一个用例,即使在使用React.memo()之后,子组件也会重新渲染
  • 如何使用useCallback()钩子来避免在将函数作为props传递给子组件时重新渲染。

希望您觉得这篇关于React记忆化的介绍有用!

关于React记忆化的常见问题解答(FAQ)

(此处与原文类似,只是对语言和表达方式进行了一些调整。)

以上是如何在REACT中实施回忆以提高性能的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn