首頁 >web前端 >js教程 >如何在REACT中實施回憶以提高性能

如何在REACT中實施回憶以提高性能

Joseph Gordon-Levitt
Joseph Gordon-Levitt原創
2025-02-09 09:00:15904瀏覽

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