不要過度使用React.useCallback()

杜尼卜發表於2020-05-15

預設檔案1589522133741.png

我部落格的一位讀者在Facebook上聯絡到我,提出了一個有趣的問題。他說,他的隊友不管在什麼情況下,都會把每一個回撥函式封裝在 useCallback() 裡面。

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);

  return <MyChild onClick={handleClick} />;
}

“每個回撥函式都應該被記住,以防止使用回撥函式的子元件被無用地重新渲染”,這是他的隊友的理由。

這句話與事實相去甚遠。此外,useCallback() 的這種用法會使元件變慢,從而損害效能。

在本文中,我將解釋如何正確使用 useCallback()

1.瞭解函式相等性檢查

在深入研究 useCallback() 用法之前,讓我們區分一下鉤子要解決的問題:函式相等性檢查。

讓我們定義一個名為 factory() 的函式,該函式返回函式:

function factory() {
  return (a, b) => a + b;
}

const sum1 = factory();
const sum2 = factory();

sum1(1, 2); // => 3
sum2(1, 2); // => 3

sum1 === sum2; // => false
sum1 === sum1; // => true

sum1sum2 是將兩個數字相加的函式,它們是由 factory() 函式建立的。

函式 sum1sum2 共享相同的程式碼源,但是它們是不同的物件,比較它們 sum1 === sum2 結果為 false

這就是JavaScript的工作方式,物件(包括函式物件)僅等於其自身。

2.useCallback() 的目的

共享相同程式碼的不同函式例項往往在React元件內部建立。

當 React 元件主體建立一個函式(例如回撥或事件處理程式)時,這個函式會在每次渲染時重新建立。

import React from 'react';

function MyComponent() {
  // handleClick在每次渲染時重新建立
  const handleClick = () => {
    console.log('Clicked!');
  };

  // ...
}

handleClickMyComponent 的每次渲染中都是一個不同的函式物件。

因為行內函數很“便宜”,所以在每次渲染時重新建立函式不是問題,每個元件有幾個行內函數是可以接受的。

然而,在某些情況下,你需要保留一個函式的一個例項:

  • 包裝在 React.memo()(或 shouldComponentUpdate )中的元件接受回撥prop。
  • 當函式用作其他hooks的依賴項時 useEffect(...,[callback])

這就是當 useCallback(callbackFun, deps) 幫助你的情況:給出相同的依賴值 deps,hook在兩次渲染之間返回相同的函式例項。

import React, { useCallback } from 'react';

function MyComponent() {
  // handleClick是同一個函式物件
  const handleClick = useCallback(() => {
    console.log('Clicked!');
  }, []);

  // ...
}

handleClick 變數將在不同的 MyComponent 的渲染之間始終擁有相同的回撥函式物件。

OIP.jpeg

3.一個好用例

想象一下,你有一個呈現大的專案列表元件:

import React from 'react';

function MyBigList({ items, handleClick }) {
  const map = (item, index) => (
    <div onClick={() => handleClick(index)}>{item}</div>;
  );
  return <div>{items.map(map)}</div>;
}

export const MyBigList = React.memo(MyBigList);

MyBigList 渲染了一個專案列表,要知道這個列表可能很大,可能有幾百個專案。要保留重新渲染的列表,可以將其封裝到 React.memo 中。

單擊一個專案時,MyBigList 的父元件需要提供專案列表和處理程式功能。

import React from 'react';

import useSearch from './fetch-items';

function MyParent({ term }) {
  const handleClick = useCallback((item) => {
    console.log('You clicked ', item);
  }, [term]);

  const items = useSearch(term);

  return (
    <MyBigList
      items={items}
      handleClick={handleClick}
    />
  );
}

handleClick 回撥由 useCallback() 記憶。只要 term 變數保持不變,useCallback() 就會返回相同的函式物件。

即使由於某些原因重新啟用了 MyParent 元件,handleClick 仍保持不變,並且不會破壞 MyBigList的記憶。

4.一個“壞”的用例

讓我們回顧一下本文簡介中的示例:

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    // handle the click event
  }, []);

  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

記住 handleClick 是否有意義?

沒有,因為呼叫 useCallback() 需要很多工作,每次渲染 MyComponent 時,都會呼叫 useCallback() Hook。

從內部來講,React確保返回相同的物件函式。即便如此,行內函數仍然在每次渲染時建立,useCallback() 只是跳過了它。

即使用 useCallback() 返回相同的函式例項,也不會帶來任何好處,因為優化要比沒有優化花費更多。

不要忘記增加的程式碼複雜性,你必須確保 useCallback() 的 deps 與您在 memoized 回撥中使用的 deps 保持同步。

只需接受每次重新渲染時建立新的函式:

import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = () => {
    // handle the click event
  };

  return <MyChild onClick={handleClick} />;
}

function MyChild ({ onClick }) {
  return <button onClick={onClick}>I am a child</button>;
}

5.總結

任何優化都會增加複雜性,任何過早新增的優化都會帶來風險,因為優化後的程式碼可能會多次更改。


原文資訊

相關文章