學習如何馴服React的useCallback鉤子

學習如何馴服React的useCallback鉤子

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

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

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

  1. 什麼是Memoization?
  2. 渲染和反應
  3. React useCallback的效能優勢
  4. React useCallback的缺點
  5. React使用回撥示例

什麼是Memoization?

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

這會對記憶體分配和效能產生重大影響,而這種壓力正是useCallback鉤子的目的所在。

React的useCallback與useMemo

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

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

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

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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])
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])
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

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

它與React如何渲染你的元件有關。React使用儲存在記憶體中的虛擬DOM來比較資料並決定更新什麼。

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

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

當React重新渲染你的元件時,它也會重新建立你在元件中宣告的函式。

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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
// 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
// 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使用得當,它可以幫助加速您的應用程式並防止元件在不需要時重新渲染。

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

 

使用React元件生成的條形圖

使用React元件生成的條形圖

 

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

React useCallback的缺點

雖然這個鉤子可以幫助你提高效能,但它也有它的缺陷。在使用useCallback(和useMemo)之前,需要考慮以下幾點:

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

意識到以上這些陷阱可以讓你省去自己跌跌撞撞的頭痛。在考慮使用useCallback時,請確保效能優勢大於缺點。

React使用回撥示例

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

Button元件有兩個props:handleClick和name。每次呈現Button時,它都會登入到控制檯。

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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" />
</>
)
}
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" /> </> ) }
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" />
</>
)
}

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// counter rendered
// button1 rendered
// button2 rendered
// counter rendered // button1 rendered // button2 rendered
// counter rendered
// button1 rendered
// button2 rendered

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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" />
</>
)
}
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" /> </> ) }
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" />
</>
)
}

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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// counter rendered
// button1 rendered
// counter rendered
// button2 rendered
// counter rendered // button1 rendered // counter rendered // button2 rendered
// counter rendered
// button1 rendered
// counter rendered
// button2 rendered

我們已經將memoization應用於我們的按鈕元件,並且傳遞給它的prop值被視為相等。這兩個handleClick函式被快取並且將被React視為同一個函式,直到依賴陣列中的專案的值發生變化(例如countOnecountTwo)。

小結

儘管useCallbackuseMemo很酷,但請記住它們有特定的用例——您不應該用這些掛鉤包裝每個函式。如果函式在計算上很複雜,那麼傳遞給記憶元件的另一個鉤子或道具的依賴關係是很好的指標,您可能希望獲得useCallback

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

評論留言