我部落格的一位讀者在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
sum1
和 sum2
是將兩個數字相加的函式,它們是由 factory()
函式建立的。
函式 sum1
和 sum2
共享相同的程式碼源,但是它們是不同的物件,比較它們 sum1 === sum2
結果為 false
。
這就是JavaScript的工作方式,物件(包括函式物件)僅等於其自身。
2.useCallback() 的目的
共享相同程式碼的不同函式例項往往在React元件內部建立。
當 React 元件主體建立一個函式(例如回撥或事件處理程式)時,這個函式會在每次渲染時重新建立。
import React from 'react';
function MyComponent() {
// handleClick在每次渲染時重新建立
const handleClick = () => {
console.log('Clicked!');
};
// ...
}
handleClick
在 MyComponent
的每次渲染中都是一個不同的函式物件。
因為行內函數很“便宜”,所以在每次渲染時重新建立函式不是問題,每個元件有幾個行內函數是可以接受的。
然而,在某些情況下,你需要保留一個函式的一個例項:
- 包裝在
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
的渲染之間始終擁有相同的回撥函式物件。
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.總結
任何優化都會增加複雜性,任何過早新增的優化都會帶來風險,因為優化後的程式碼可能會多次更改。
原文資訊
- 原文:https://dmitripavlutin.com/dont-overuse-react-usecallback/
- 作者:Dmitri Pavlutin
- 翻譯:微信公眾號《前端外文精選》