到目前為止,我們添加到依賴陣列的依賴項是字符串。字符串,布爾值,數字等 JavaScript 原生型態都是可以有比較性的。
字符串將等於預期的字符串。
1 | if ( "tainan" === "tainan") { |
但是,當我們開始比較物件,陣列和函式時,這就有不同的結果了。 例如,如果我們比較兩個陣列:
1 | if ( [1, 2, 3] !== [1, 2, 3]) { |
這兩組陣列不相等,即使它們的長度和項目看起來相同。 這是因為它們是外觀相似的陣列,但卻是兩個不同的實例 (instance)。如果我們創建一個變數來保存該陣列的值然後進行比較,我們將看到預期的結果:
1 | const array = [1, 2, 3]; |
深度檢查依賴項 (Deep Checking Dependencies)
在 JavaScript 中,陣列、物件和函式只有在它們完全相同的實例時才是相同的。 那麼,這與 useEffect 依賴陣列有何關係? 為了了解這一點,我們需要一個盡可能頻繁強制渲染的組件。我們要構建一個掛鉤,該掛鉤使組件在每次按下任何鍵時都會渲染:
1 | <div id="react-container"></div> |
- 第 7 行 useAnyKeyToRender 是我們自定義的掛鉤。
- 第 8 行 我們要強制渲染的工作就是調用狀態更改函式。我們不在乎狀態值。我們只需要狀態函式:forceRender。
- 第 11 行 在第一次渲染組件時,我們將監聽 keydown 事件。按下某個鍵時,我們將通過調用 forceRender 強制組件進行渲染。
通過將此掛鉤添加到組件,我們只需按一下任何鍵就可以強制其重新渲染。我們將此掛鉤直接加入 App 組件。現在,每次我們按下一個鍵,都會渲染 App 組件。每次渲染 App 組件時都會調用 useEffect 將 “fresh render” 記錄到控制台。
現在讓我們在 App 組件中調整 useEffect 來引用 word 值。 如果 word 值更改,我們將重新渲染:
1 | const word = "tainan"; |
現在不會在每個 keydown 事件上調用 useEffect,我們只會在首次渲染之後以及 word 值更改的時後會調用此方法。word 的值沒有改變,因此不會發生後續的重新渲染。 將原生類型或數字添加到依賴陣列,會按預期方式工作。 該 effect 只被調用一次。
如果我們改用字串陣列,會發生什麼?
1 | const words = ["hello", "tainan", "!"]; |
變數 words 是一個陣列,因為每次渲染都會宣告一個新陣列,是一個新實例,JavaScript 認定 words 已被更改,因此每次都會觸發重新渲染調用 “fresh render” effect。
將變數 words 宣告在 App 組件之外可以解決該問題:
1 | const words = ["hello", "tainan", "!"]; |
在這種情況下,依賴陣列引用在函式外部宣告的 words 實例,在第一個渲染之後不會再次調用 “fresh render” effect,因為 words 與上次渲染時是同一個實例。對於此示例來說,這是一個很好的解決方案,但並非總是可能 (或建議) 在函式範圍之外定義變數。有時,傳遞給依賴陣列的值需要宣告在函式範圍內。
例如,我們可能需要從類似 children 的 React 屬性建立 words 陣列:
1 | const WordCount = ({ children = "" }) => { |
App 組件包含一些字串,它們是 WordCount 組件的子代,WordCount 組件將 children 作為屬性。 然後,我們在組件中將這些單字使用 .split 方法轉換為單字陣列。我們希望該組件僅在單字更改時才會重新渲染。但是事與願違,一旦按下任何鍵,我們就會在控制台中看到 “fresh render”。
React 提供了一種避免這些額外渲染的方法。此問題的解決方案是另一個掛鉤:useMemo。
useMemo 調用一個函式來計算已記憶的值 (memoized value)。在計算機科學中,記憶 (memoization) 是一種用於提高性能的技術。在記憶功能中,函式調用的結果將會保存並緩存。 然後,當使用相同的輸入再次調用該函式時,將返回緩存的值。在 React 中,useMemo 允許我們將緩存的值與其自身進行比較,以查看其是否實際被更改。
首先,讓我們導入 useMemo 掛鉤。
1 | const { useState, useEffect, useMemo } = React; |
然後,我們將傳遞給一個用於計算和創建記憶值的函式給 useMemo 掛鉤,這個函式返回的值將被用來設定 words:
1 | const words = children.split(" "); |
與 useEffect 一樣,useMemo 也有依賴陣列:
1 | const words = useMemo(() => children.split(" ")); |
當我們在 useMemo 中不包括依賴陣列時,將會在每次渲染時重新計算 words。依賴陣列控制何時應調用回調函式。這裡將 children 值加到依賴陣列:
1 | const words = useMemo(() => children.split(" "), [children]); |
words 陣列取決於 children 屬性,如果子元素發生變化,我們應該為反映該變化的 words 計算一個新值。現在,useMemo 將在組件第一次渲染時以及 children 屬性更改時為 words 計算一個新值。
在創建 React 應用程序時,需要好好理解 useMemo 掛鉤的用途。
useCallback 的使用方式類似 useMemo,但它用於記億函式而不是值。
1 | const fn = () => { |
fn 是一個函式。它是 useEffect 的依賴項,但是就像 words 一樣,JavaScript 假定 fn 每次渲染都是不同的實例。因此,它會在每次按鍵渲染時觸發 effect。 這不是很理想的,這可以使用 useCallback 來包裝函式:
1 | const fn = useCallback(() => { |
useCallback 會記住 fn 的函數值。就像 useMemo 和 useEffect 一樣,它也有依賴陣列作為第二個參數。在這裡,由於依賴陣列是空陣列,因此只會創建了一次回調函式。
其實我們可以使用 useMemo 達到一樣的結果:
1 | const fn = useMemo(() => { |
useCallback 只省掉幾個程式碼字元而已。
When to useLayoutEffect
我們知道,渲染始終在 useEffect 之前。 渲染首先發生,然後所有的 effect 按順序運行,並且可以完全訪問渲染中的所有值。
在 React 還有另一種 effect 掛鉤:useLayoutEffect。
useLayoutEffect 可以在渲染週期的特定時刻被調用。這一系列的週期事件如下:
- Render
- 調用 useLayoutEffect
- 瀏覽器繪製畫面:組件的元素實際添加到 DOM 的時間
- 調用 useEffect
我們可以通過添加一些簡單的控制台訊息來觀察這些事件:
1 | <div id="react-container"></div> |
在 App 組件中,useEffect 是第一個掛鉤,其後是 useLayoutEffect,但我們看到 useLayoutEffect 在 useEffect 之前被調用。
useLayoutEffect |
useLayoutEffect 在渲染之後但在瀏覽器繪製更改之前被調用。在大多數情況下,useEffect 是完成工作的正確工具,但是如果您的 effect 對於瀏覽器繪畫(螢幕上 UI 元素的外觀或位置)至關重要,則可能要使用 useLayoutEffect。
例如,您可能需要在調整窗口大小時獲取元素的寬度和高度。
1 | const useWindowSize = () => { |
窗口的寬度和高度是我們的組件在瀏覽器繪製之前可能需要的信息。useLayoutEffect 用於在繪製之前計算窗口的寬度和高度。
另一個使用 useLayoutEffect 的示例是在追踪鼠標的位置時:
1 | const useMousePosition = () => { |
繪製螢幕時很有可能會使用鼠標的 x 和 y 位置。 useLayoutEffect 可用於幫助在繪製之前準確計算這些位置。