React 提供了許多優化程式碼的方式,但是我還沒好好地去認識它們,趁這次機會重新認識一下 memo()、useMemo、useCallback。
什麼是 React.memo()
在解釋什麼是 React.memo() 是什麼之前,先來看個簡單範例,稍微複習一下 React 的運作方式。
首先,我們主要的 App 元件長這樣:
const App = () => {
//記錄 APP 渲染次數
const [rerenderedIndex, setRerenderedIndex] = useState(0);
//記錄 Square 當前的顏色
const [color, setColor] = useState("red");
console.log("App rerendered " + rerenderedIndex);
return (
<div className="App">
<Square color={color} />
<button onClick={() => setRerenderedIndex((prev) => prev + 1)}>
Rerenderd
</button>
<button onClick={() => setColor(color === "red" ? "black" : "red")}>
Change Color
</button>
</div>
);
};
畫面上,會有一個正方形(Square 元件)、兩個 buttons。Rerendered 按鈕用來觸發重新渲染,而 Change Color 按鈕用來改變正方形的顏色。
除此之外,我們讓它在每一次渲染時都在 console 印出訊息,藉此來追蹤渲染的情況。
Square 元件會接受一個 color prop 來決定正方形的顏色,同樣的我們讓它在重新渲染時都在 console 印出訊息。
const Square = ({ color }) => {
console.log("Square rerendered " + color);
return (
<div
style={{
margin: "50px auto",
width: "50px",
height: "50px",
backgroundColor: color
}}
></div>
);
};
實際畫面長這樣:
可以看到,console 內因初次渲染所以已經印出了兩條訊息。
當我們點擊 Rerendered 按鈕 4 次時...
會發現 App 和 Square 元件皆重新渲染了四次。這情況是正常的,雖然 Square 元件的顏色並沒有改變,但因為它是 App 底下的子元件,所以一樣會被重新渲染。
嗯...既然 Square 沒有發生任何變化根本不需要重新渲染啊!(嗅到了可進一步優化的機會 XD
沒錯,現在就是派上 React.memo() 的好時機。
我們把元件 Square 傳入 memo() 內,並把 memo() 回傳的新元件丟入變數 MemoedSquare。
const MemoedSquare = memo(Square);
接著把原本的 Square 改成 MemoedSquare。
一樣點擊 Rerendered 按鈕 4 次,看看會發生什麼事。
發現只有 App 重新渲染了四次,而 MemoedSquare 並未跟著重新渲染!
memo 語法
import { memo } from 'react';
const MyComponent = memo(function MyComponent(props) {
/* render using props */
});
用 memo() 去將想要暫存的元件包覆起來。
memo 意即 memoization
memo 其實是 memoization 的縮寫,是一種優化的技巧。會將函式回傳的值暫存起來,當下次遇到一樣的 input 時,就能快速的輸出已被暫存起來的值,提升效能。
我們使用 lodash (JS函式庫)提供的 memoize() 函式來實際看看 memoization 的效果。
依照剛剛針對 memoization 的解釋,猜猜看會印出什麼呢?
答案是:
//color changed: red
//color changed: yellow
//color changed: Blue
React 裡面的 memoization
只不過 React memo() 的效果和一般 memoization 有點不同。
一般 memoization 的運作方式是只要同樣的 input,也就是曾經出現過的 input,它的回傳值就會被暫存起來。
但是 React.memo() 則會拿這一次的 input (props)與上一次的 input (props)做比較,若相同的話則會避免重新渲染,不同的話則會再次渲染,就算 input 曾經出現過,但只要跟上一次不一樣就還是會重新渲染。
React.memo() 的使用時機
當今天有一個元件,經常會傳入相同的 props,就可以考慮使用 React.memo() 來減少不必要的渲染。
反之,若元件的 props 經常改變,就不需要使用 React.memo(),使用了不但沒有優化到效能,甚至會產生危害。
使用 React.memo() 的注意事項
This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs. --React Docs
要記住 React.memo()的目的是用來「優化」,不能把它用來當作避免重新渲染的一種邏輯控制。
React.memo() 失效了?!
在範例中,我們的 Square 的 color prop 是原始型別--字串。要是今天,我們傳入的 props 是一個物件呢?
<MemoedSquare params={{color}} />
接著一樣點擊 Rerendered 按鈕 4次。
天啊怎麼會!memo() 失效了!memoedSquare 元件繼續被重新渲染了。
因為 React 是使用 shallow compare 的方式來進行比較。所謂的 shallow compare 指的是「對於基本型別比較的是值(value);對於物件型別比較的是記憶體位置(reference)」。
因此,每一次 App 重新渲染時,都會重新產生一個新的 {color}
物件,雖然物件的內容不變,但由於記憶體位置發生改變,所以 memo() 就不會阻止元件重新渲染。
memo() 的第二個參數
要解決這個問題的方式之一就是,傳入第二個參數到 memo() 內。
memo(Component, arePropsEqual?)
- Component:傳入我們想要取得 memoized 版本的元件
- arePropsEqual?:傳入一個用來告訴 memo() 是否要重新渲染的函式,而這個函式有兩個參數(元件的上一個 props & 新 props )。
const MemoedSquare = memo(Square,(prev, next)=>{
return prev.params.color === next.params.color;
});
如此一來,memo() 又能正常運作啦~
useMemo() 登場
除了在 memo() 傳入第二個參數函式之外,也可透過使用 useMemo() 來解決問題。
useMemo 語法
const cachedValue = useMemo(calculationFn, dependencies array)
- calculationFn:計算暫存值的函式。
- dependencies array:將計算暫存值有使用到的變數都放進 dependecies array。React 會使用 Object.is() 來比較陣列內的值是否與上一次相同。
useMemo 功用
當 dependecies array 沒有改變時,useMemo 會回傳暫存值。看範例來說明:
const params = useMemo(() => ({ color }), [color]);
當 color 沒變的時候,useMemo 將確保回傳完全相同(記憶體位置相同)的 {color} 物件。如此一來, memo() 就可防止 MemoedSquare 重新渲染,成功解決問題。
useMemo 使用時機
1.當今天某一個值的計算比較複雜、會花費較多的時間時,就可以考慮使用 useMemo。
const value = useMemo(
() => numbers.reduce((prev, cur) => prev + cur, 0),
[numbers]
);
2.當今天某一個值要作為 prop 傳入一個被 memo() 包覆的元件。就像範例中,雖然產生 {color}
物件並不是什麼複雜的計算,但為了避免每一次渲染 App 都重新產生一個全新的 {color}
物件,進而導致 memo()失效,我們一樣可以使用 useMemo() 來防止 color 相同時,產生全新物件。
3.當今天某一個值可能會被放入其他 hooks 的 dependencies array。
同場加映:useCallback
要是我們今天在 MemoedSquare 元件又新增一個 onClick prop 呢?
<MemoedSquare params={params} onClick={()=>{}}/>
你會發現,memo() 又再一次失效了。
原因和我們一開始設定 params={{color}}
類似,每一次渲染 App 都又會重新產生一個 onClick 函式。解決此問題的思維也和之前一樣,那我們就把onClick 函式暫存起來就沒問題了。
const onClick = useCallback(()=>{}, [])
...
<MemoedSquare params={params} onClick={onClick}/>
...
看一下官方文件對 useCallback 的解釋:
useCallback is a React Hook that lets you cache a function definition between re-renders.
重點在 function definition!不同於 useMemo(),useCallback 暫存的是函式定義本身,而並不是計算過後的值。
Resources
https://youtu.be/DEPwA3mv_R8
https://beta.reactjs.org/reference/react/useMemo