0%

Enhancing Components with Hooks (Ⅱ)

到目前為止,我們添加到依賴陣列的依賴項是字符串。字符串,布爾值,數字等 JavaScript 原生型態都是可以有比較性的。

字符串將等於預期的字符串。

1
2
3
if ( "tainan" === "tainan") {
console.log("Tainan!!");
}

但是,當我們開始比較物件,陣列和函式時,這就有不同的結果了。 例如,如果我們比較兩個陣列:

1
2
3
if ( [1, 2, 3] !== [1, 2, 3]) {
console.log("但它們是一樣的");
}

這兩組陣列不相等,即使它們的長度和項目看起來相同。 這是因為它們是外觀相似的陣列,但卻是兩個不同的實例 (instance)。如果我們創建一個變數來保存該陣列的值然後進行比較,我們將看到預期的結果:

1
2
3
4
const array = [1, 2, 3];
if ( array === array) {
console.log("因為它是同一個實例 (instance)");
}

深度檢查依賴項 (Deep Checking Dependencies)

在 JavaScript 中,陣列、物件和函式只有在它們完全相同的實例時才是相同的。 那麼,這與 useEffect 依賴陣列有何關係? 為了了解這一點,我們需要一個盡可能頻繁強制渲染的組件。我們要構建一個掛鉤,該掛鉤使組件在每次按下任何鍵時都會渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div id="react-container"></div>

<script type="text/babel">
const { useState, useEffect } = React;
const { render } = ReactDOM;

const useAnyKeyToRender = () => {
const [, forceRender] = useState();

useEffect(() => {
window.addEventListener("keydown", forceRender);
return () => window.removeEventListener("keydown", forceRender);
}, []);
};

const App = () => {
useAnyKeyToRender();

useEffect(() => {
console.log("fresh render");
});

return <h1>Open the console</h1>;
};

render(
<App />,
document.getElementById("react-container")
);
</script>
  • 第 7 行 useAnyKeyToRender 是我們自定義的掛鉤。
  • 第 8 行 我們要強制渲染的工作就是調用狀態更改函式。我們不在乎狀態值。我們只需要狀態函式:forceRender。
  • 第 11 行 在第一次渲染組件時,我們將監聽 keydown 事件。按下某個鍵時,我們將通過調用 forceRender 強制組件進行渲染。

通過將此掛鉤添加到組件,我們只需按一下任何鍵就可以強制其重新渲染。我們將此掛鉤直接加入 App 組件。現在,每次我們按下一個鍵,都會渲染 App 組件。每次渲染 App 組件時都會調用 useEffect 將 “fresh render” 記錄到控制台。

現在讓我們在 App 組件中調整 useEffect 來引用 word 值。 如果 word 值更改,我們將重新渲染:

1
2
3
4
const word = "tainan";
useEffect(() => {
console.log("fresh render");
}, [word]);

現在不會在每個 keydown 事件上調用 useEffect,我們只會在首次渲染之後以及 word 值更改的時後會調用此方法。word 的值沒有改變,因此不會發生後續的重新渲染。 將原生類型或數字添加到依賴陣列,會按預期方式工作。 該 effect 只被調用一次。

如果我們改用字串陣列,會發生什麼?

1
2
3
4
const words = ["hello", "tainan", "!"];
useEffect(() => {
console.log("fresh render");
}, [words]);

變數 words 是一個陣列,因為每次渲染都會宣告一個新陣列,是一個新實例,JavaScript 認定 words 已被更改,因此每次都會觸發重新渲染調用 “fresh render” effect。

將變數 words 宣告在 App 組件之外可以解決該問題:

1
2
3
4
5
6
7
8
9
10
11
const words = ["hello", "tainan", "!"];

const App = () => {
useAnyKeyToRender();

useEffect(() => {
console.log("fresh render");
}, [words]);

return <h1>Open the console</h1>;
};

在這種情況下,依賴陣列引用在函式外部宣告的 words 實例,在第一個渲染之後不會再次調用 “fresh render” effect,因為 words 與上次渲染時是同一個實例。對於此示例來說,這是一個很好的解決方案,但並非總是可能 (或建議) 在函式範圍之外定義變數。有時,傳遞給依賴陣列的值需要宣告在函式範圍內。

例如,我們可能需要從類似 children 的 React 屬性建立 words 陣列:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const WordCount = ({ children = "" }) => {
useAnyKeyToRender();

const words = children.split(" " );

useEffect(() => {
console.log("fresh render");
}, [words]);

return (
<>
<p>{children}</p>
<p>
<strong>{words.length} - words</strong>
</p>
</>
);
};

const App = () => {
return <WordCount>You are not going to believe this but...</WordCount>
};

App 組件包含一些字串,它們是 WordCount 組件的子代,WordCount 組件將 children 作為屬性。 然後,我們在組件中將這些單字使用 .split 方法轉換為單字陣列。我們希望該組件僅在單字更改時才會重新渲染。但是事與願違,一旦按下任何鍵,我們就會在控制台中看到 “fresh render”。

React 提供了一種避免這些額外渲染的方法。此問題的解決方案是另一個掛鉤:useMemo。

useMemo 調用一個函式來計算已記憶的值 (memoized value)。在計算機科學中,記憶 (memoization) 是一種用於提高性能的技術。在記憶功能中,函式調用的結果將會保存並緩存。 然後,當使用相同的輸入再次調用該函式時,將返回緩存的值。在 React 中,useMemo 允許我們將緩存的值與其自身進行比較,以查看其是否實際被更改。

首先,讓我們導入 useMemo 掛鉤。

useMemo
1
const { useState, useEffect, useMemo } = React;

然後,我們將傳遞給一個用於計算和創建記憶值的函式給 useMemo 掛鉤,這個函式返回的值將被用來設定 words:

1
2
3
4
5
6
7
8
const words = children.split(" ");



const words = useMemo(() => {
const words = children.split(" ");
return words;
}, []);

與 useEffect 一樣,useMemo 也有依賴陣列:

useMemo
1
const words = useMemo(() => children.split(" "));

當我們在 useMemo 中不包括依賴陣列時,將會在每次渲染時重新計算 words。依賴陣列控制何時應調用回調函式。這裡將 children 值加到依賴陣列:

useMemo
1
const words = useMemo(() => children.split(" "), [children]);

words 陣列取決於 children 屬性,如果子元素發生變化,我們應該為反映該變化的 words 計算一個新值。現在,useMemo 將在組件第一次渲染時以及 children 屬性更改時為 words 計算一個新值。

在創建 React 應用程序時,需要好好理解 useMemo 掛鉤的用途。

useCallback 的使用方式類似 useMemo,但它用於記億函式而不是值。

useCallback
1
2
3
4
5
6
7
8
9
const fn = () => {
console.log("Hello");
console.log("Tainan");
};

useEffect(() => {
console.log("fresh render");
fn();
}, [fn]);

fn 是一個函式。它是 useEffect 的依賴項,但是就像 words 一樣,JavaScript 假定 fn 每次渲染都是不同的實例。因此,它會在每次按鍵渲染時觸發 effect。 這不是很理想的,這可以使用 useCallback 來包裝函式:

useCallback
1
2
3
4
const fn = useCallback(() => {
console.log("Hello");
console.log("Tainan");
}, []);

useCallback 會記住 fn 的函數值。就像 useMemo 和 useEffect 一樣,它也有依賴陣列作為第二個參數。在這裡,由於依賴陣列是空陣列,因此只會創建了一次回調函式。

其實我們可以使用 useMemo 達到一樣的結果:

useMemo
1
2
3
4
5
6
7
8
const fn = useMemo(() => {
const fn = () => {
console.log("Hello");
console.log("Tainan");
};

return fn;
}, []);

useCallback 只省掉幾個程式碼字元而已。

When to useLayoutEffect

我們知道,渲染始終在 useEffect 之前。 渲染首先發生,然後所有的 effect 按順序運行,並且可以完全訪問渲染中的所有值。

在 React 還有另一種 effect 掛鉤:useLayoutEffect。

useLayoutEffect 可以在渲染週期的特定時刻被調用。這一系列的週期事件如下:

  1. Render
  2. 調用 useLayoutEffect
  3. 瀏覽器繪製畫面:組件的元素實際添加到 DOM 的時間
  4. 調用 useEffect

我們可以通過添加一些簡單的控制台訊息來觀察這些事件:

render cycle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="react-container"></div>

<script type="text/babel">
const { useEffect, useLayoutEffect } = React;
const { render } = ReactDOM;

const App = () => {
useEffect(() => console.log("useEffect"));
useLayoutEffect(() => console.log("useLayoutEffect"));
return <div>ready</div>
};

render (
<App />,
document.getElementById("react-container")
);
</script>

在 App 組件中,useEffect 是第一個掛鉤,其後是 useLayoutEffect,但我們看到 useLayoutEffect 在 useEffect 之前被調用。

useLayoutEffect
useEffect

useLayoutEffect 在渲染之後但在瀏覽器繪製更改之前被調用。在大多數情況下,useEffect 是完成工作的正確工具,但是如果您的 effect 對於瀏覽器繪畫(螢幕上 UI 元素的外觀或位置)至關重要,則可能要使用 useLayoutEffect。

例如,您可能需要在調整窗口大小時獲取元素的寬度和高度。

useWindowSize
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const useWindowSize = () => {
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);

const resize = () => {
setWidth(window.innerWidth);
setHeight(window.innerHeight);
};

useLayoutEffect(() => {
window.addEventListener("resize", resize);
resize();
return () => window.removeEventListener("resize", resize);
}, []);

return [width, height]
};

窗口的寬度和高度是我們的組件在瀏覽器繪製之前可能需要的信息。useLayoutEffect 用於在繪製之前計算窗口的寬度和高度。

另一個使用 useLayoutEffect 的示例是在追踪鼠標的位置時:

useMousePosition
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const useMousePosition = () => {
const [x, setX] = useState(0);
const [y, setY] = useState(0);

const setPosition = ({ x, y }) => {
setX(x);
setY(y);
};

useLayoutEffect(() => {
window.addEventListener("mousemove", setPosition);
return () => window.removeEventListener("mousemove", setPosition);
}, []);

return [x, y];
};

繪製螢幕時很有可能會使用鼠標的 x 和 y 位置。 useLayoutEffect 可用於幫助在繪製之前準確計算這些位置。