5-8 了解定義函式的適當位置以及 useCallback 的使用
本單元對應的專案分支為:create-function-with-use-callback
。
#
單元核心這個單元的主要目標包含:
- 了解 useCallback 的使用方式
- 了解當函式不需要共用時,可以直接將函式定義在 useEffect 內並呼叫使用
- 了解當函式需要共用時,可以把函式拉到 useEffect 外定義
- 了解如何讓函式與元件的資料狀態解耦,以利未來程式的拆檔與管理
在上一個單元中,我們透過 async function
搭配 Promise.all
的使用,等到取得所有需要的資料後才更新畫面。但在昨天的程式碼中,我們把 fetchData
這個 async function
定義在 useEffect()
內,為什麼我們要這麼做?這麼做有什麼好處呢?還有其他做法嗎?
#
當函式不需要共用時,可以直接定義在 useEffect 內先來看一下,在上一個單元中我們怎麼呼叫 fetchData
這個方法:
現在當我們把 fetchData
這個函式定義在 useEffect
中時,因為在整個 useEffect
中沒有相依於任何 React 內的資料狀態(state
或 props
),因此在 useEffect
第二個參數的 dependencies 陣列中仍然可以留空就好(即,[]
),也因為 dependencies 陣列內都固定沒有元素,因此只會在畫面第一次轉譯完成後被呼叫到而已。
重點
當函式不需要共用時,可以直接定義在 useEffect 中。
這種在 useEffect
內定義函式並呼叫的作法本身沒有任何問題,但眼尖的朋友可能也會發現,在上一個單元中,原本用來「重新整理」的按鈕現在已經失效了,因為原先用來呼叫 API 的 fetchCurrentWeather
和 fetchWeatherForecast
這兩個方法,現在都變成是回傳 Promise
而不是直接在取得資料後呼叫 setWeatherElement
來更新 React 元件內的資料狀態。
那麼如果要讓「重新整理」的按鈕恢復原有的功能,可以怎麼做呢?
#
當函式需要共用時,可以拉到 useEffect 外現在我們因為在 useEffect
中以及在使用者點擊重新整理的按鈕時,都需要更新資料,因此比較好的做法不是在 onClick
中重複撰寫一次和 fetchData
一模一樣的程式碼,而是把這個 fetchData
的方法搬到 useEffect
外,而後就可以在 useEffect
和 onClick
時都去呼叫這個方法。
也就是把程式碼改成:
現在就可以在 useEffect
中和 onClick
中共用 fetchData
這個方法!
#
useCallback 的使用除了直接把函式拉到 useEffect 外去定義之外,你可能還有看過有一種做法是使用 useCallback
把這個會被共用的函式給包起來,寫法上會像這樣:
這個 useCallback
是做什麼用的呢?
我們知道只要 React 內的資料狀態有變動時,這整個用來產生 React 元件的 Function 都會再重新執行一次,也就是說,每次只要資料狀態有變更時,fetchData
這個函式就會被重新宣告定義一次,每一次的 fetchData
內容雖然都是一樣的,但因為都會經歷重新宣告與賦值的過程,所以其實是「新的」函式。
這裡所謂「內容相同」但卻又是「新的」到底是什麼意思呢?
我們曾經在前面的單元中提到,dependencies 陣列中元素是否相同是透過 Object.is()
這個方法來判斷,這個判斷方式就和 ===
的方式大同小異,在 JavaScript 中要判斷兩個物件是否相同時,並不是直接判斷物件中的屬性名稱和屬性值相同就可以,而是要看它們是否參照到同一個記憶體位置。舉例來說,當我們定義了兩個物件,即使這兩個物件內的屬性名稱和屬性值都一樣,使用 ===
來判斷也會得到 false
:
重點
在許多開發的說明文件中,經常會看到變數或值使用 foo
或 bar
,雖然對初學者來說,這個變數很惱人,不清楚為什麼要這樣做,但實際上有經驗的開發者一看就知道這是「沒意義」的變數,單純只是示範用的,因此算是一種共同的默契。撰寫文件的人不需要花不必要的心力去思考變數名稱,而閱讀文件的人一看到 foo
、bar
時,就會知道這只是說明用的變數。
而在 JavaScript 中「函式」本質上其實就是物件的一種,因此即使宣告了兩個內容相同的函式,在等值的判斷上還是不同的:
回到 App 元件中的 fetchData
函式來看,現在只要 App 元件的資料狀態一有變動,就會重新產生一個內容相同但實際上並不相同(參照到不同的記憶體位址)的函式。一般來說,在元件重新轉譯時一併重新定義元件內的函式並不會有什麼太大的影響,但有少部分的情況(記住是很少數的時候),你可能會希望不要讓這個 fetchData
每次都被重新宣告,例如當這個函式有可能被放到 dependencies 陣列中的情況。
舉例來說,假設現在把 fetchData
這個函式放到 dependencies 陣列中:
這時候將會出現無窮迴圈的情況,這是因為對於 useEffect
dependencies 陣列中的 fetchData
來說,每一次元件經過重新轉譯後,雖然 fetchData 函式中的內容相同,但每次的 fetchData
其實都是不同的(指稱到不同記憶體位置)。
上面我們有提到,在 JavaScript 中即使物件或函式內容完全相同的情況下,使用 Object.is
來判斷兩者是否相同時還是會得到 false
,那麼有什麼時候是會得到 true
呢?簡單來說,當這兩個「東西」是指稱到同一個「位址」時就會是 true
。
舉例來說:
當兩個物件指稱到同一個位址時,這時候透過 JavaScript 的 ===
就會判斷這兩個是相同的。
上面雖然是使用物件來舉例,但套用到函式時的概念也一樣。如果想要在 React 元件重新轉譯後,仍可以指稱到同一個記憶體位置的函式時,就可以使用 useCallback
這個 React Hooks 來把 fetchData
這個函式給保存下來,讓它不會每次因為元件重新轉譯(render)就變成一個「新的」函式。
這裡先來看一下 useCallback
這個 React Hooks 可以怎麼使用。
useCallback
的用法和 useEffect
幾乎一樣,同樣可以帶入兩個參數,第一個參數是一個函式,在這個函式中可以去執行原本函式要做的事、或是去呼叫原本的函式,第二個參數一樣是 dependencies 陣列。不同的地方是 useCallback
回傳的是一個函式,只有當 dependencies 有改變時,useCallback 才會回傳一個新的函式:
透過 useCallback
就可以避免 Functional Component 每次重新執行後,函式內容明明相同,但卻會重新定義新函式的情況。
#
實際使用 useCallback回到原本的 App 元件的程式碼,可以試著把 useCallback
實際應用到臺灣好天氣中。整個流程如下:
- 從 react 中載入
useCallback
這個 React Hook
- 使用
useCallback
並將回傳的函式取名為fetchData
- 在 useCallback 的 dependencies 陣列中帶入空陣列
- 使用
useCallback
後,只要useEffect
中的 dependencies 沒有改變,它回傳的fetchData
就可以指稱到同一個函式。再把這個fetchData
放到useEffect
的 dependencies 後,就不會重新呼叫useEffect
內的函式。
當我們把 fetchData
中原本的函式內容用 useCallback
包起來後,只要 useCallback()
中 dependencies 陣列中的元素沒有改變,那麼這個 fetchData
就會一直指稱到相同的函式,而不會在每次函式重新轉譯時就再次建立新的 fetchData
函式。
#
是否有必要使用 useCallback?實際上 useCallback
被使用到的機會沒有這麼高,useCallback 雖然能夠避免 React 元件在重新轉譯後再次建立新的函式,但多數時候即使重複建立這些函式,對於瀏覽器和電腦的負擔並不會增加太多,相較之下,為了比較 useCallback
中的 dependencies 陣列元素是否相同還可能會消耗更多效能,因此多數的時候並不需要使用到 useCallback 這個方法。
只有在一些情況下,例如當 useEffect
的 dependencies 陣列會需要相依於某個函式時,開發者可以透過 useCallback
來把這個函式保存下來,以避免這個函式在元件重新轉譯後又是「新的」,進而導致 useEffect
每次都會重新執行的情況。
以目前「臺灣好天氣」的程式碼來說, useCallback
是可用可不用的,在這裡比較大的用意是向讀者示範 useCallback
這個方法的使用,因為實務上仍會看到開發者使用 useCallback
來作為效能提升的方式之一。
#
整理 React Hooks 中函式定義的位置在這一系列的章節中,你會發現同樣的 fetchCurrentWeather
, fetchWeatherForecast
或 fetchData
這幾個函式,會根據不同的使用情境,定義在程式碼不同的位置,這裡讓我們來整理一下。
#
當函式不需要共用時,直接將函式定義在 useEffect 內並呼叫使用假設 fetchCurrentWeather
只有需要在頁面第一次載入時會被呼叫,而沒有其他情況會需要被呼叫時(例如,點擊重新整理按鈕)。這時候比較好的做法是在 useEffect
中去定義這個函式,並且呼叫它。
前面有提到過,只要元件重新轉譯,定義在元件內的函式都會因為元件的重新轉譯而被重新宣告,但是當我們把函式的定義放在 useEffect
中時,因為 useEffect
的內容只有在其 dependencies 陣列中的元素有所不同時才會被執行,因此可以減少函式不斷被重新定義的次數。舉例來說:
#
當函式需要共用時,把函式拉到 useEffect 外定義現在,如果 fetchCurrentWeather
同時需要在 useEffect 中呼叫,而且同時也需要在其他情況下(例如,使用者點擊時)被呼叫時,可以把這個函式拉到 useEffect 外定義:
這種情況下,雖然 fetchCurrentWeather
可能會因為元件重新轉譯而不斷被重新定義,但一般來說對於瀏覽器的負擔或效能的影響並不會太大,並不一定需要使用 useCallback
來特別處理。
#
讓函式與資料狀態解耦最後,如果在 useEffect
中需要拉取來自多支 API 的資料時,比較好的做法是讓拉取 API 的方法與元件的資料狀態(即,state 或 props)脫鉤,也就是說,不要在該函式中使用到 useState
回傳的 state 或 setSomething,或者是父層元件傳入的 props。如此拉取 API 的函式就可以定義在 React 元件外面,未來更方便將程式碼做拆檔與管理:
我們只需要將更新資料狀態的方法統一在一個函式( fetchData
)中管理即可,至於這個 fetchData
是應該要定義在 useEffect
內或外,則又回到前面提到的兩個情況,如果 fetchData
只會在 useEffect
中被使用,而沒有共用的情況,那可以直接在 useEffect
內定義該函式並呼叫即可;但若該函式可能會在不同地方被呼叫,則需要把該函式拉到 useEffect
外加以定義。
#
換你了!建立可以在 useEffect 中被共用的函式在這個單元中我們整理了 React Hooks 中定義函式適當的位置,並且說明了 useCallback
這個 Hooks,useCallback
雖然多半的情況下,用與不用的差異不會太大,但這個單元中剛好是個可以實際使用它的機會,就請讀者試試看吧。
現在請你建立一個可以同時在 useEffect
與 onClick
中共同使用的 fetchData
函式,讓重新整理的按鈕恢復它原有的功能。你可以參考這樣的流程:
- 將
useCallback
方法從 react 套件import
進來 - 將
fetchData
拉到 useEffect 外 - 使用
useCallback
以確保fetchData
不會因為元件重新轉譯而變成新的,記得要帶入useCallback
的 dependencies 陣列 - 在
useEffect
內(初次載入頁面)與點擊重新整理按鈕時呼叫fetchData
- 把
fetchData
放到useEffect
的 dependencies array 中
本單元相關之網頁連結、完整程式碼與程式碼變更部分可於 create-function-with-use-callback
分支檢視:https://github.com/pjchender/learn-react-from-hook-realtime-weather-app/tree/create-function-with-use-callback