學習如何馴服 React 的 useCallback Hook

已發表: 2022-04-27

React.js 近年來廣受歡迎,這已經不是什麼秘密了。 它現在是許多互聯網上最傑出的參與者(包括 Facebook 和 WhatsApp)的首選 JavaScript 庫。

它興起的主要原因之一是在 16.8 版本中引入了鉤子。 React 鉤子允許您在不編寫類組件的情況下利用 React 功能。 現在,帶有鉤子的功能組件已成為開發人員使用 React 的首選結構。

在這篇博文中,我們將深入研究一個特定的鉤子useCallback因為它涉及函數式編程的一個基本部分,即記憶化。 您將確切地知道如何以及何時使用useCallback掛鉤並充分利用其性能增強功能。

準備好? 讓我們潛入吧!


什麼是記憶?

記憶是當一個複雜的函數存儲它的輸出以便下次使用相同的輸入調用它時。 它類似於緩存,但在本地級別上。 它可以跳過任何復雜的計算並更快地返回輸出,因為它已經計算過了。

這會對內存分配和性能產生重大影響,而這種壓力正是useCallback掛鉤的目的所在。

React 的 useCallback 與 useMemo

在這一點上,值得一提的是useCallback與另一個名為useMemo的鉤子很好地配對。 我們將同時討論它們,但在這篇文章中,我們將重點關注useCallback作為主要主題。

關鍵區別在於useMemo返回一個記憶值,而useCallback返回一個記憶函數。 這意味著useMemo用於存儲計算值,而useCallback返回一個您可以稍後調用的函數。

這些鉤子會給你一個緩存的版本,除非它們的依賴項之一(例如狀態或道具)發生變化。

讓我們看一下這兩個函數的作用:

 import { useMemo, useCallback } from 'react' const values = [3, 9, 6, 4, 2, 1] // This will always return the same value, a sorted array. Once the values array changes then this will recompute. const memoizedValue = useMemo(() => values.sort(), [values]) // This will give me back a function that can be called later on. It will always return the same result unless the values array is modified. const memoizedFunction = useCallback(() => values.sort(), [values])

上面的代碼片段是一個人為的示例,但顯示了兩個回調之間的區別:

  1. memoizedValue將成為數組[1, 2, 3, 4, 6, 9] 。 只要 values 變量保持不變, memoizedValue也會保持不變,並且它永遠不會重新計算。
  2. memoizedFunction將是一個返回數組[1, 2, 3, 4, 6, 9]的函數。

這兩個回調的好處是它們會被緩存並一直存在,直到依賴數組發生變化。 這意味著在渲染時,它們不會被垃圾收集。

React.js 現在是許多互聯網巨頭的首選 JavaScript 庫,包括 Facebook 和 WhatsApp。 在本指南中了解更多信息️ 點擊推

渲染和反應

為什麼在 React 中記憶很重要?

它與 React 如何渲染你的組件有關。 React 使用存儲在內存中的虛擬 DOM 來比較數據並決定更新什麼。

虛擬 DOM 幫助 React 提高性能並讓您的應用程序保持快速。 默認情況下,如果您的組件中的任何值發生更改,整個組件將重新渲染。 這使得 React 對用戶輸入具有“反應性”,並允許屏幕更新而無需重新加載頁面。

您不想渲染組件,因為更改不會影響該組件。 這就是通過useCallbackuseMemo進行記憶的地方。

當 React 重新渲染你的組件時,它也會重新創建你在組件中聲明的函數。

請注意,當比較一個函數與另一個函數的相等性時,它們總是為假的。 因為函數也是一個對象,所以它只會等於它自己:

// these variables contain the exact same function but they are not equal const hello = () => console.log('Hello Matt') const hello2 = () => console.log('Hello Matt') hello === hello2 // false hello === hello // true

換句話說,當 React 重新渲染你的組件時,它會將在你的組件中聲明的任何函數都視為新函數。

這在大多數情況下都很好,簡單的函數很容易計算並且不會影響性能。 但其他時候,當您不希望函數被視為新函數時,您可以依靠useCallback來幫助您。

你可能會想,“我什麼時候不希望一個函數被視為一個新函數?” 好吧,在某些情況下useCallback更有意義:

  1. 您正在將該函數傳遞給另一個也被記憶的組件( useMemo
  2. 你的函數有一個需要記住的內部狀態
  3. 您的函數是另一個鉤子的依賴項,例如useEffect

React useCallback 的性能優勢

useCallback被適當地使用時,它可以幫助加速你的應用程序並防止組件在不需要時重新渲染。

例如,假設您有一個組件,它獲取大量數據並負責以圖表或圖形的形式顯示該數據,如下所示:

一個彩色條形圖,比較 PHP、MySQL、Reddis 和外部(其他)的整體事務時間(以毫秒為單位)。
使用 React 組件生成的條形圖。

假設您的數據可視化組件的父組件重新渲染,但更改的道具或狀態不會影響該組件。 在這種情況下,您可能不想或不需要重新渲染它並重新獲取所有數據。 避免這種重新渲染和重新獲取可以節省用戶的帶寬並提供更流暢的用戶體驗。

因停機時間和 WordPress 問題而苦苦掙扎? Kinsta 是旨在節省您時間的託管解決方案! 查看我們的功能

React useCallback 的缺點

雖然這個鉤子可以幫助你提高性能,但它也有它的缺陷。 在使用useCallback (和useMemo )之前需要考慮的一些事情是:

  • 垃圾收集: React 將丟棄其他尚未記憶的函數以釋放內存。
  • 內存分配:與垃圾回收類似,你擁有的記憶功能越多,需要的內存就越多。 另外,每次你使用這些回調時,React 中都有一堆代碼需要使用更多的內存來為你提供緩存的輸出。
  • 代碼複雜性:當您開始在這些鉤子中包裝函數時,您會立即增加代碼的複雜性。 現在需要更多地了解為什麼使用這些鉤子並確認它們被正確使用。

意識到上述陷阱可以避免你自己絆倒它們的頭痛。 在考慮使用useCallback時,請確保性能優勢將超過缺點。

反應使用回調示例

下面是一個帶有 Button 組件和 Counter 組件的簡單設置。 Counter 有兩個狀態並渲染出兩個 Button 組件,每個組件將更新 Counter 組件狀態的一個單獨部分。

Button 組件有兩個 props: handleClick和 name。 每次呈現 Button 時,它都會記錄到控制台。

 import { useCallback, useState } from 'react' const Button = ({handleClick, name}) => { console.log(`${name} rendered`) return <button onClick={handleClick}>{name}</button> } const Counter = () => { console.log('counter rendered') const [countOne, setCountOne] = useState(0) const [countTwo, setCountTwo] = useState(0) return ( <> {countOne} {countTwo} <Button handleClick={() => setCountOne(countOne + 1)} name="button1" /> <Button handleClick={() => setCountTwo(countTwo + 1)} name="button1" /> </> ) }

在此示例中,無論何時單擊任一按鈕,您都會在控制台中看到:

 // counter rendered // button1 rendered // button2 rendered

現在,如果我們將useCallback應用到我們的handleClick函數並將我們的 Button 包裝在React.memo中,我們可以看到useCallback為我們提供了什麼。 React.memo類似於useMemo並允許我們記憶一個組件。

 import { useCallback, useState } from 'react' const Button = React.memo(({handleClick, name}) => { console.log(`${name} rendered`) return <button onClick={handleClick}>{name}</button> }) const Counter = () => { console.log('counter rendered') const [countOne, setCountOne] = useState(0) const [countTwo, setCountTwo] = useState(0) const memoizedSetCountOne = useCallback(() => setCountOne(countOne + 1), [countOne) const memoizedSetCountTwo = useCallback(() => setCountTwo(countTwo + 1), [countTwo]) return ( <> {countOne} {countTwo} <Button handleClick={memoizedSetCountOne} name="button1" /> <Button handleClick={memoizedSetCountTwo} name="button1" /> </> ) }

現在,當我們單擊任一按鈕時,我們只會看到我們單擊以登錄控制台的按鈕:

 // counter rendered // button1 rendered // counter rendered // button2 rendered

我們已經將 memoization 應用於我們的按鈕組件,並且傳遞給它的 prop 值被視為相等。 這兩個handleClick函數被緩存,並且將被 React 視為同一個函數,直到依賴數組中的項目的值發生變化(例如countOnecountTwo )。

準備好了解如何以及何時使用 useCallback 掛鉤,以及如何充分利用其性能增強功能? 從這裡開始! 點擊推文

概括

useCallbackuseMemo一樣酷,記住它們有特定的用例——你不應該用這些鉤子來包裝每個函數。 如果函數計算複雜,則另一個鉤子的依賴關係或傳遞給記憶化組件的道具是您可能希望達到的良好指標useCallback

我們希望這篇文章能幫助你理解這個高級的 React 功能,並幫助你在使用函數式編程的過程中獲得更多的信心!