我博客的一位讀者在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
在深刻研究 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
。fetch
這就是JavaScript的工做方式,對象(包括函數對象)僅等於其自身。優化
共享相同代碼的不一樣函數實例每每在React組件內部建立。spa
當 React 組件主體建立一個函數(例如回調或事件處理程序)時,這個函數會在每次渲染時從新建立。
import React from 'react'; function MyComponent() { // handleClick在每次渲染時從新建立 const handleClick = () => { console.log('Clicked!'); }; // ... }
handleClick
在 MyComponent
的每次渲染中都是一個不一樣的函數對象。
由於內聯函數很「便宜」,因此在每次渲染時從新建立函數不是問題,每一個組件有幾個內聯函數是能夠接受的。
然而,在某些狀況下,你須要保留一個函數的一個實例:
React.memo()
(或 shouldComponentUpdate
)中的組件接受回調prop。useEffect(...,[callback])
這就是當 useCallback(callbackFun, deps)
幫助你的狀況:給出相同的依賴值 deps
,hook在兩次渲染之間返回相同的函數實例。
import React, { useCallback } from 'react'; function MyComponent() { // handleClick是同一個函數對象 const handleClick = useCallback(() => { console.log('Clicked!'); }, []); // ... }
handleClick
變量將在不一樣的 MyComponent
的渲染之間始終擁有相同的回調函數對象。
想象一下,你有一個呈現大的項目列表組件:
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
的記憶。
讓咱們回顧一下本文簡介中的示例:
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>; }
任何優化都會增長複雜性,任何過早添加的優化都會帶來風險,由於優化後的代碼可能會屢次更改。
原文信息