是時候用 useMemo,useCallback,React's memo API 優化性能了?

2021-03-02 前端知識小站

在這篇文章中將介紹如何在 React 應用程式中使用 useMemo、useCallback、React's memo API 去優化性能,但是需要注意的是不要濫用這些 API。

不要將 React 的 useMemo Hook 與 React 的 memo API 混淆了。useMemo 被用於緩存值,memo API 被用於包裹 React 組件去阻止組件重新渲染。

不要將 React 的 useMemo Hook 與 React 的 useCallback Hook 混淆了。useMemo 被用於緩存值,useCallback 被用於緩存函數。

在 React 中怎麼使用 useMemo

React 中的 useMemo 被用於優化 React 函數組件的計算性能。接下來我將會通過一個例子來說明函數組件的性能問題,然後再使用 useMemo 來解決它。

讓我們以下面的 React 應用程式為例,在這個例子中渲染了一個用戶列表,並且我們可以使用用戶名去過濾用戶,在點擊按鈕時才發生過濾,在輸入框中輸入用戶名時不過濾。代碼如下:

import React from 'react';
 
const users = [
  { id: 'a', name: 'Robin' },
  { id: 'b', name: 'Dennis' },
];
 
const App = () => {
  const [text, setText] = React.useState('');
  const [search, setSearch] = React.useState('');
 
  const handleText = (event) => {
    setText(event.target.value);
  };
 
  const handleSearch = () => {
    setSearch(text);
  };
 
  const filteredUsers = users.filter((user) => {
    return user.name.toLowerCase().includes(search.toLowerCase());
  });
 
  return (
    <div>
      <input type="text" value={text} onChange={handleText} />
      <button type="button" onClick={handleSearch}>
        Search
      </button>
 
      <List list={filteredUsers} />
    </div>
  );
};
 
const List = ({ list }) => {
  return (
    <ul>
      {list.map((item) => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
};
 
const ListItem = ({ item }) => {
  return <li>{item.name}</li>;
};
 
export default App;

雖然每一次往輸入框中輸入值時 filteredUsers 的值不會發生變化,但是 filter 的回調函數在每一次往輸入框中輸入值時都會執行。我們可以這樣修改代碼:

function App() {
  ...
 
  const filteredUsers = users.filter((user) => {
    console.log('Filter function is running ...');
    return user.name.toLowerCase().includes(search.toLowerCase());
  });
 
  ...
}

在這個小的 React 應用程式中這不是什麼大的問題,但是如果數組中有一批大的數據並且在每一次鍵盤輸入時 filter 的回調函數都會執行,我們可能會感覺到應用程式很慢。因此,我們使用 React 的 useMemo Hook 緩存函數的返回值,僅僅在 useMemo 依賴項發生變化的時候函數才會重新運行,從上面的代碼中我們可以看出 filter 的回調函數的依賴項是 search。所以使用 useMemo 代碼如下:

function App() {
  ...
  const filteredUsers = React.useMemo(() => {
    return users.filter(user => {
        console.log('Filter function is running ...');
        return user.name.toLowerCase().includes(search.toLowerCase());
    })
  },[search])
  ...
}

現在,filter 的回調函數僅僅在search發生變化時才會執行,text的值發生變化時函數不會執行,因為text不是 useMemo 的依賴項。你可以自己嘗試一下,現在往輸入框中輸入值時控制臺上不會出現 console.log 的列印值,僅僅到點擊按鈕時才會有列印。

你可能會想知道為什麼不在所有的值計算中使用 useMemo Hook 或者 React 為什麼不默認給所有的值計算使用 useMemo Hook。這是因為在每一次組件重新渲染時,useMemo Hook 都會比較依賴數組中的每一個依賴項以決定是否要重新計算值,進行依賴項的比較可能比重新計算值更耗費性能。

總結:如果給 useMemo 傳遞了依賴項數組,只有在依賴項發生變化時 useMemo 才會重新計算新的值(如果依賴項數據是個空數組,只在組件第一次渲染時計算值),如果沒有給 useMemo 傳遞依賴數組,在每一次渲染時都會重新計算新的值。

在 React 中怎麼使用 memo API

在 React 中可以用 memo API 來優化函數組件的渲染行為。接下來我們將會通過一個例子來說明函數組件的性能問題,然後再使用 memo API 來解決它。

讓我們以下面的React應用程式為例,在這個例子中渲染了一個用戶列表,我們可以往用戶列表中新增用戶。代碼如下:

import React from 'react';
import { v4 as uuidv4 } from 'uuid';
 
const App = () => {
  console.log('Render: App');
  const [users, setUsers] = React.useState([
    { id: 'a', name: 'Robin' },
    { id: 'b', name: 'Dennis' },
  ]);
 
  const [text, setText] = React.useState('');
 
  const handleText = (event) => {
    setText(event.target.value);
  };
 
  const handleAddUser = () => {
    setUsers(users.concat({ id: uuidv4(), name: text }));
  };
 
  return (
    <div>
      <input type="text" value={text} onChange={handleText} />
      <button type="button" onClick={handleAddUser}>
        Add User
      </button>
 
      <List list={users} />
    </div>
  );
};
 
const List = ({ list }) => {
  console.log('Render: List');
  return (
    <ul>
      {list.map((item) => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
};
 
const ListItem = ({ item }) => {
  console.log('Render: ListItem');
  return <li>{item.name}</li>;
};
 
export default App;

你會發現每往輸入框中輸入一個字符,所有的組件都會重新渲染

// 往輸入框中輸入一個字符之後

Render: App
Render: List
Render: ListItem
Render: ListItem

在這個小的 React 應用程式中這不會有什麼問題,但是如果這裡是一個大的用戶列表,用戶在往輸入框中輸入字符就會感覺到慢,在這個時候我們可以使用 memo API 進行優化。改寫代碼如下:

const List = React.memo(({ list }) => {
  console.log('Render: List');
  return (
    <ul>
      {list.map((item) => (
        <ListItem key={item.id} item={item} />
      ))}
    </ul>
  );
});
 
const ListItem = ({ item }) => {
  console.log('Render: ListItem');
  return <li>{item.name}</li>;
};

現在我們往輸入框中輸入字符,僅僅 App 組件會被重新渲染。React.memo 會檢查 List 組件的 props 是否發生變化,如果沒有變化會跳過 List 組件的重新渲染。在這個例子中,往輸入框中輸入字符不會導致 List 組件的 list prop 發生變化,所以 List 不會重新渲染,因而 ListItem 也不會重新渲染。看上去我們不需要給 ListItem 使用 memo API,但是如果往用戶列表中新增一個用戶,在控制臺中你將看到以下輸出:

// 添加一個新的用戶之後
 
Render: App
Render: List
Render: ListItem
Render: ListItem
Render: ListItem

當添加一個新的用戶會導致 List 組件被重新渲染,但是 ListItem 被渲染三次,我們希望只渲染一個新的用戶,而不是所有的用戶,所以使用 React.memo 改寫 ListItem:

const ListItem = React.memo(({ item }) => {
  console.log('Render: ListItem');
  return <li>{item.name}</li>;
});

經過改寫,往用戶列表中添加一個新的用戶,你會看到如下的輸出:

// a添加一個新的用戶之後
 
Render: App
Render: List
Render: ListItem

只有新的 ListItem 被渲染了,用戶列表之前的 ListItem 保持不變。

你可能會想知道為什麼不在所有的組件中使用 memo API 或者 React 為什麼不默認給所有的組件使用 memo API。這是因為 memo API 會將新的 props 與之前的 props 進行比較以決定是否重新渲染組件,進行 props 的比較可能比重新渲染更耗費性能。

總之,當你的 React 組件變慢並且您想要改進它們的性能時,React的 memo API 就會很有用。這通常發生在數據量大的組件中,比如在一個數據點發生變化,許多組件必須重新渲染的巨大列表時。

在 React 中怎麼使用 useCallback

在 React 中可以用 useCallback hook 來優化函數組件的渲染行為。接下來我們將會通過一個例子來說明函數組件的性能問題,然後再使用 useCallback 來解決它。

讓我們以下面的React應用程式為例,在這個例子中渲染了一個用戶列表,我們可以往用戶列表中新增用戶也可以從用戶列表中刪除用戶。代碼如下:

import React from 'react';
import { v4 as uuidv4 } from 'uuid';
 
const App = () => {
  console.log('Render: App');
  const [users, setUsers] = React.useState([
    { id: 'a', name: 'Robin' },
    { id: 'b', name: 'Dennis' },
  ]);
 
  const [text, setText] = React.useState('');
 
  const handleText = (event) => {
    setText(event.target.value);
  };
 
  const handleAddUser = ()  =>{
    setUsers(users.concat({ id: uuidv4(), name: text }));
  };
 
  const handleRemove = (id) => {
    setUsers(users.filter((user) => user.id !== id));
  };
 
  return (
    <div>
      <input type="text" value={text} onChange={handleText} />
      <button type="button" onClick={handleAddUser}>
        Add User
      </button>
 
      <List list={users} onRemove={handleRemove} />
    </div>
  );
};
 
const List = ({ list, onRemove }) => {
  console.log('Render: List');
  return (
    <ul>
      {list.map((item) => (
        <ListItem key={item.id} item={item} onRemove={onRemove} />
      ))}
    </ul>
  );
};
 
const ListItem = ({ item, onRemove }) => {
  console.log('Render: ListItem');
  return (
    <li>
      {item.name}
      <button type="button" onClick={() => onRemove(item.id)}>
        Remove
      </button>
    </li>
  );
};
 
export default App;

如果你在一個 react 應用中寫入上面的代碼,你會發現每次往輸入框中輸入字符,控制臺都會列印:

Render: App
Render: List
Render: ListItem
Render: ListItem

我們希望往輸入框中輸入字符,只是 App 組件重新渲染,App 的子組件不重新渲染。根據上文的介紹,我們使用 memo API 來阻止子組件的更新,於是改寫 List 和 ListItem 組件:

const List = React.memo(({ list, onRemove }) => {
  console.log('Render: List');
  return (
    <ul>
      {list.map((item) => (
        <ListItem key={item.id} item={item} onRemove={onRemove} />
      ))}
    </ul>
  );
})

const ListItem = React.memo(({ item, onRemove }) => {
  console.log('Render: ListItem');
  return (
    <li>
      {item.name}
      <button type="button" onClick={() => onRemove(item.id)}>
        Remove
      </button>
    </li>
  );
});

經過改寫之後你發現每往輸入框中輸入一個字符,List 和 ListItem 還是會重新渲染。

讓我們看一下傳遞給 List 組件的 props

const App = () => {
  // How we're rendering the List in the App component 
  return (
    //...
    <List list={users} onRemove={handleRemove} />
  )
}

只要沒有從 List 組件中添加或刪除任何項,即使用戶在輸入框中輸入一些內容後就算 App 組件重新渲染,List 也應該保持原樣。因此,罪魁禍首是 onRemove 回調處理程序。

每當用戶在輸入框中輸入一些內容後 App 組件重新渲染,App 組件中的 handleRemove 都會被重新定義。往 List 組件中傳遞一個新的函數作為 prop,List 注意到一個 prop 與之前的渲染相比發生了變化。這就是每當用戶在輸入欄位框中輸入一些內容後 List 和 ListItem 會重新渲染的原因。

現在我們使用 useCallback Hook 改寫代碼:

const App = () => {
  ...
  // 依賴項數組作為 useCallback 的第二個參數
  const handleRemove = React.useCallback(
    (id) => setUsers(users.filter((user) => user.id !== id)),
    [users]
  );
  ...
};

依賴項數組中的任何一項發生變化都會導致 handleRemove 被重新定義。如果 users 被改變了,handleRemove 會被重新定義,List 和 ListItem 也應該被重新渲染。用戶在輸入框中輸入一些內容不會導致 handleRemove 被重新定義,它保持原樣。子組件的 prop 不會發生改變,所以不會重新渲染。

你可能會想知道為什麼不在所有的函數中使用 useCallback Hook 或者 React 為什麼不默認給所有的函數使用 useCallback Hook。這是因為在每一次重新渲染時 useCallback Hook 會比較依賴數組中的每一個依賴項以決定是否要重新定義函數,進行依賴項的比較可能比重新渲染更耗費性能。

總之,React 的 useCallback Hook 用於緩存函數。當函數被當作 props 傳遞給其他組件我們不用擔心函數會因為父組件重新渲染而被重新初始化。然而,正如你所看到的,當與 React 的 memo API 一起使用時,React 的 useCallback 鉤子開始發揮作用

總結:如果給 useCallback 傳遞了依賴項數組,只有在依賴項發生變化時 useCallback 才會重新定義函數(如果依賴項數據是個空數組,只在組件第一次渲染時重新定義函數),如果沒有給 useCallback 傳遞依賴數組,在每一次渲染時都會重新定義函數。useCallback(fn, deps) 等同於 useMemo(() => fn, deps)。

相關焦點

  • 超性感的React Hooks(十一)useCallback、useMemo
    這個時候,我們思考一個問題,當我們重複調用summation(100)時,函數內部的循環計算是不是有點冗餘?因為傳入的參數一樣,得到的結果必定也是一樣,因此如果傳入的參數一致,是不是可以不用再重複計算直接用上次的計算結果返回呢?當然可以,利用閉包能夠實現我們的目的。
  • 【Hooks】:[組]When to use React.useCallback()
    For example, if a child component that accepts a callback relies on a referential equality check (eg: React.memo() or shouldComponentUpdate) to prevent unnecessary re-renders when its
  • 不要過度使用React.useCallback()
    此外,useCallback() 的這種用法會使組件變慢,從而損害性能。在本文中,我將解釋如何正確使用 useCallback()。1.了解函數相等性檢查在深入研究 useCallback() 用法之前,讓我們區分一下鉤子要解決的問題:函數相等性檢查。
  • 手寫ReactHook核心原理,再也不怕面試官問我ReactHook原理
    import React ,{useState,memo}from 'react';import ReactDOM from 'react-dom';import '.手寫useCallback useCallback的使用當我們試圖給一個子組件傳遞一個方法的時候,如下代碼所示import React ,{useState,memo}from 'react';import ReactDOM from 'react-dom';
  • 為什麼要在函數組件中使用React.memo?
    初探memo首先讓我們用一個例子走進React.memo的世界呆呆的函數組件 - 沒有使用memo❝對於一個函數組件來說,如果沒有使用React.memo就好比是一個人沒有腦子,就笨笨的呆呆的不信我們就來看下面的Demo❞點擊訪問演示Demo讓我們來分析下上圖發生的流程:
  • 你可能不知道的 React Hooks
    本文是譯文,原文地址是:https://medium.com/@sdolidze/the-iceberg-of-react-hooks-af0b588f43fbReact Hooks 與類組件不同,它提供了用於優化和組合應用程式的簡單方式,並且使用了最少的樣板文件。
  • React-Redux 使用 Hooks
    React-Redux 提供了一系列 符合 Hooks 風格的 use api,來替代現有 connect api。: Function)該 api 功能類似傳統 connect api 傳入的 mapStateToProps, 但是表現形式還是有些許差別選擇器 Selector 函數可以返回任意類型的數據,而不僅限於 對象當 dispathc 一個 action 的時候,useSelector() 函數將對 Selector 選擇器函數的返回值和上一個狀態的返回值進行比較
  • React:useHooks小竅門
    我們使用React.memo將它包裹起來,但是我們仍然需要尋找性能問題 :/// 因此我們添加useWhyDidYouUpdate並在控制臺查看將會發生什麼const Counter = React.memo(props => { useWhyDidYouUpdate('Counter', props); return <div style={props.style}
  • React hooks 最佳實踐【更新中】
    裡或者用不同的useCallback包起來,所依賴的變量,也要儘可能的與邏輯相關聯,這樣可以儘可能的避免性能損耗和bug的產出。設置為僅在初次渲染,那麼會造成這種情況:第一次渲染的時候正常,但是在第二次渲染的時候,執行到的第一個鉤子函數是:const [lastName, setLastName] = useState('yeyung');這時候,react會去執行頂層的棧中的方法,也就是我們後續的操作都往前挪了一位。
  • 函數式編程看React Hooks(一)簡單React Hooks實現
    useEffect再看看 useEffect, 先來看看使用方法。useEffect(callback,dep?), 以下是一個非常簡單的使用例子。_deps.args || hasChangedDeps) { callback(); _deps.args = args; }}演示地址:https://codesandbox.io/s/ecstatic-glitter-w9kq7至此,我們也實現了單個 useEffect。
  • React系列二十一 - Hook(二)高級使用
    所以,useReducer只是useState的一種替代品,並不能替代Redux。1.2. useCallbackuseCallback實際的目的是為了進行性能的優化。如何進行性能的優化呢?事實上,經過一些測試,並沒有更加節省內存,因為useCallback中還是會傳入一個函數作為參數;所以並不存在increment2每次創建新的函數,而increment1不需要創建新的函數這種性能優化;那麼,為什麼說useCallback是為了進行性能優化呢?
  • React v16.6.0 發布,新增 lazy, memo 和 contextType 等便捷特性
    按照官方的說法,React.memo() 主要是用於函數式組件,作為 PureComponent 的替代方案;React.lazy()  則是使用 Suspense 進行代碼分割的方法;新增的 contextType 則是作為一種更符合使用習慣的方式用於從類訂閱上下文。
  • react中關於hook介紹及其使用
    >在每次渲染的時候都去調用hooks,解決的方式如下面所示useEffect(() => { })用一個特殊變量的去觸發hook,如下面所示,count指的就是這個特殊的變量,該hook觸發,只會是count的值改變時
  • 淺談React 中的state 與useEffect
    useEffect 本來就沒有一定要搭配什麼東西而用。想要理解useEffect,並不需要他們。useEffect 的用途就跟它的名字一樣:「拿來處理side effects」用的。useEffect 就是useEffect,它跟其他那些useCallback 或是useMemo 並沒有什麼關聯,用途也完全不一樣。
  • react的核心api-前端進階
    範例:用戶輸入事件,創建 EventHandle.js import { useState, useEffect } from "react"; function ClockFunc() { // useState創建一個狀態和修改該狀態的函數
  • 「不容錯過」手摸手帶你實現 React Hooks
    例如,useState 是允許你在 React 函數組件中添加 state 的 Hook。來檢查代碼錯誤    {      "plugins": ["react-hooks"],      // ...
  • React-hooks入坑指南
    性能也更好。Hooks 使用指南useState通常我們在使用 class 組件時,會在constructor中初始化state。constructor只會在組件實例化時執行一次。同樣的,使用useState可以實現上述的操作。
  • 手撕React學算法:React.Children-遞歸和對象池
    React.Children裡面的幾個api都是數組處理相關的,這裡就分析一下map實現原理,看看它與Array.map有什麼不同。
  • SPA單頁面應用優化VUE性能優化
    Spa單頁面應用比如vue、react、angular都是屬於單頁面應用,那麼如何優化呢?咱們拿vue舉例。SPA單頁面應用優化VUE性能優化單頁面應用離不開構建工具比如webpack、fis3、gulp等,目前應用最多的就是webpack,咱們就用webpack