不要過分使用React.useCallback()

默認文件1589522133741.png

我博客的一位讀者在Facebook上聯繫到我,提出了一個有趣的問題。他說,他的隊友無論在什麼狀況下,都會把每個回調函數封裝在 useCallback() 裏面。javascript

import React, { useCallback } from 'react';

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

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

「每一個回調函數都應該被記住,以防止使用回調函數的子組件被無用地從新渲染」,這是他的隊友的理由。前端

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

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

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 結果爲 falsefetch

這就是JavaScript的工做方式,對象(包括函數對象)僅等於其自身。優化

2.useCallback() 的目的

共享相同代碼的不一樣函數實例每每在React組件內部建立。spa

當 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.總結

任何優化都會增長複雜性,任何過早添加的優化都會帶來風險,由於優化後的代碼可能會屢次更改。


原文信息

相關文章
相關標籤/搜索