首頁  >  文章  >  web前端  >  聊聊怎麼利用Memoization提升React效能

聊聊怎麼利用Memoization提升React效能

青灯夜游
青灯夜游轉載
2022-03-29 20:02:102246瀏覽

這篇文章帶大家了解一下Memoization,介紹為什麼需要 Memoization,以及 React中實作 Memoization以提升效能的方法,希望對大家有幫助!

聊聊怎麼利用Memoization提升React效能

在本教程中,我們將學習如何在 React 中實作 Memoization。 Memoization 透過快取函數呼叫的結果並在再次需要時傳回這些快取的結果來提高效能。

我們將介紹以下內容:

  • React 是如何渲染視圖的?
  • 為什麼需要 Memoization?
  • 如何在函數元件和類別元件中實作 Memoization?
  • 注意事項

本文假設你對 React 中的類別和函數元件有基本的了解。

如果你想查閱這些主題,可以查看React 官方文件components and props

https://reactjs.org/docs/components-and- props.html

聊聊怎麼利用Memoization提升React效能

React 是如何渲染視圖的?

在討論 React 中的 Memoization 細節之前,讓我們先來看看 React 是如何使用虛擬 DOM 渲染 UI 的。 【相關推薦:Redis影片教學

常規 DOM 基本上包含一組用樹的形式儲存的節點。 DOM 中的每個節點代表一個 UI 元素。每當應用程式中出現狀態變更時,該 UI 元素及其所有子元素的對應節點都會在 DOM 樹中更新,然後觸發 UI 重繪。

在高效的 DOM 樹演算法的幫助下,更新節點的速度更快,但重繪的速度很慢,當該 DOM 具有大量 UI 元素時,可能會影響效能。因此,在 React 中引入了虛擬 DOM。

這是真實 DOM 的虛擬表示。現在,每當應用程式的狀態有任何變更時,React 不會直接更新真正的 DOM,而是建立一個新的虛擬 DOM。然後 React 會將此新的虛擬 DOM 與先前創建的虛擬 DOM 進行比較,找到有差異的地方(譯者註:也就是找到需要更新節點),然後進行重繪。

根據這些差異,虛擬 DOM 能更有效率地更新真正的 DOM。這提高了效能,因為虛擬 DOM 不會簡單地更新 UI 元素及其所有子元素,而是有效地僅更新實際 DOM 中必要且最小的變更。

為什麼需要 Memoization?

在上一節中,我們看到了 React 如何使用虛擬 DOM 有效地執行 DOM 更新操作來提高效能。在本節中,我們將介紹一個例子,該例子解釋了為了進一步提高效能而需要使用 Memoization。

我們將建立一個父類,包含一個按鈕,用於遞增名為 count 的變數。父元件也呼叫了子元件,並向其傳遞參數。我們也在render 方法中加入了console.log() 語句:

//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>{this.state.count}</h2>
        <Child name={"joe"} />
      </div>
    );
  }
}

export default Parent;

此範例的完整程式碼可在CodeSandbox 上查看。

我們將建立一個Child 類,該類別接受父元件傳遞的參數並將其顯示在UI 中:

//Child.js
class Child extends React.Component {
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;

每當我們點擊父元件中的按鈕時,count 值都會變更。由於 state 變化了,因此父元件的 render 方法被執行了。

傳遞給子元件的參數在每次父元件重新渲染時都沒有改變,因此子元件不應重新渲染。然而,當我們運行上面的程式碼並繼續遞增count 時,我們得到了以下輸出:

Parent render
Child render
Parent render
Child render
Parent render
Child render

你可以在這個sandbox 中體驗上述範例,並查看控制台的輸出結果。

從輸出中,我們可以看到,當父元件重新渲染時,即使傳遞給子元件的參數保持不變,子元件也會重新渲染。這將導致子元件的虛擬 DOM 與先前的虛擬 DOM 執行差異檢查。由於我們的子元件中沒有變更且重新渲染時的所有 props 都沒有改變,所以真正的 DOM 不會被更新。

真正的 DOM 不會進行不必要地更新對效能確實是有好處,但是我們可以看到,即使子元件中沒有實際更改,也會建立新的虛擬 DOM 並執行差異檢查。對於小型 React 元件,這種效能消耗可以忽略不計,但對於大型元件,效能影響會很大。為了避免這種重新渲染和虛擬 DOM 的差異檢查,我們使用 Memoization。

React 中的 Memoization

在 React 应用的上下文中,Memoization 是一种手段,每当父组件重新渲染时,子组件仅在它所依赖的 props 发生变化时才会重新渲染。如果子组件所依赖的 props 中没有更改,则它不会执行 render 方法,并将返回缓存的结果。由于渲染方法未执行,因此不会有虚拟 DOM 创建和差异检查,从而实现性能的提升。

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

类组件实现 Memoization

为了在类组件中实现 Memoization,我们将使用 React.PureComponentReact.PureComponent 实现了 shouldComponentUpdate(),它对 stateprops 进行了浅比较,并且仅在 props 或 state 发生更改时才重新渲染 React 组件。

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

//Child.js
class Child extends React.PureComponent { // 这里我们把 React.Component 改成了 React.PureComponent
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;

此示例的完整代码显示在这个 sandbox 中。

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

Parent render
Child render
Parent render
Parent render

对于首次渲染,它同时调用父组件和子组件的 render 方法。

对于每次增加 count 后的重新渲染,仅调用父组件的 render 函数。子组件不会重新渲染。

函数组件实现 Memoization

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

以下是函数组件的代码:

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child); // 这里我们给子组件添加 HOC 实现 Memoization

同时还将父组件转换为了函数组件,如下所示:

//Parent.js
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}</h2>
      <Child name={"joe"} />
    </div>
  );
}

此示例的完整代码可以在这个 sandbox 中看到。

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

Parent render
Child render
Parent render
Parent render
Parent render

React.memo() 存在的问题

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

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

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

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = () => {
    console.log("handler");    // 这里的 handler 函数将会被传递给子组件
  };

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}

子组件代码将保持原样。我们不会在子组件中使用父组件传递来的函数:

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child);

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

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

接下来,我们将介绍如何解决此问题。

通过 useCallback() 来避免更多的重复渲染

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

为了避免每次渲染父组件时都重新创建函数,我们将使用一个名为 useCallback() 的 React Hook。Hooks 是在 React 16 中引入的。要了解有关 Hooks 的更多信息,你可以查看 React 的官方 hooks 文档,或者查看 `React Hooks: How to Get Started & Build Your Own"。

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

以下是 useCallback() 示例:

const handleClick = useCallback(() => {
  //Do something
}, [x,y]);

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

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

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = useCallback(() => { // 给 handler 函数使用 useCallback()
    console.log("handler");
  }, []);

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}

子组件代码将保持原样。

此示例的完整代码这个 sandbox 中。

当我们在上述代码的父组件中增加 count 时,我们可以看到以下输出:

Parent render
Child render
Parent render
Parent render
Parent render

由于我们对父组件中的 handler 使用了 useCallback() 钩子,因此每次父组件重新渲染时,都不会重新创建 handler 函数,并且会将 handler 的 Memoization 版本传递到子组件。子组件将进行浅比较,并注意到 handler 函数的引用没有更改,因此它不会调用 render 方法。

值得注意的事

Memoization 是一种很好的手段,可以避免在组件的 state 或 props 没有改变时对组件进行不必要的重新渲染,从而提高 React 应用的性能。你可能会考虑为所有组件添加 Memoization,但这并不一定是构建高性能 React 组件的方法。只有在组件出现以下情况时,才应使用 Memoization:

  • 固定的输入有固定的输出时
  • 具有较多 UI 元素,虚拟 DOM 检查将影响性能
  • 多次传递相同的参数

总结

在本教程中,我们理解了:

  • React 是如何渲染 UI 的
  • 为什么需要 Memoization
  • 如何在 React 中通过函数组件的 React.memo() 和类组件的 React.PureComponent 实现 Memoization
  • 通过一个例子展示,即使在使用 React.memo() 之后,子组件也会重新渲染
  • 如何使用 useCallback() 钩子来避免在函数作为 props 传递给子组件时产生重新渲染的问题

希望这篇 React Memoization 的介绍对你有帮助!

原文地址:https://www.sitepoint.com/implement-memoization-in-react-to-improve-performance/

原文作者:Nida Khan

更多编程相关知识,请访问:编程视频!!

以上是聊聊怎麼利用Memoization提升React效能的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除