Hook是在React 16.8之后增加的一項新功能,能夠幫助我們在不寫class的情況下使用state和其他React的相關特性。關于如何使用Hook官網有很多介紹,但理論是一碼事,實踐又是另一碼事。
如果寫過一段時間的React,我們知道通過使用 useMemo
和 useCallback
能夠幫助我們的代碼提高性能,避免component在re-render時的無效計算。于是,慢慢的大家會發現前端代碼里到處都是 useMemo
和 useCallback
,不僅影響代碼的可讀性,而且也不利于debug。
本文會通過舉例等方式告訴大家,其實我們前端工程中幾乎90%的 useMemo
和 useCallback
都是可以移除的,而且代碼不會有任何問題,且啟動加載速度會更快。
為什么需要 useMemo和 useCallback
為了在re-render之間能夠進行緩存。如果hook包裹了某個值或者方法,react會在初始渲染時對其進行緩存,并在多次渲染時對該保存值進行引用。如果沒有hook,非原生類型如數組、對象和方法會在每次重新渲染時重新創建。例如:
const a = { "test": 1 };
const b = { "test": 1'};
console.log(a === b); // will be false
const c = a; // "c" is just a reference to "a"
console.log(a === c); // will be true
另一個更貼近react代碼的例子:
const Component = () => {
const a = { test: 1 };
useEffect(() => {
// "a" will be compared between re-renders
}, [a]);
// the rest of the code
};
a
是 useEffect
的一個依賴值,React每次重新渲染 Component
時都會與其之前的值進行對比。a
是在 Component
中定義的一個對象,因此每次重新渲染都會重新創建。因此“渲染前”的a
與“渲染后”的 a
比較結果為不想等,因此 useEffect
也會在每次重新渲染時被觸發。
為了避免上述情況,我們可以對a
使用 useMemo
:
const Component = () => {
// preserving "a" reference between re-renders
const a = useMemo(() => ({ test: 1 }), []);
useEffect(() => {
// this will be triggered only when "a" value actually changes
}, [a]);
// the rest of the code
};
現在只有當a
的值真的發生變化時才會觸發useEffect
useCallback
的使用與上述類似,只是它更常用于方法:
const Component = () => {
// preserving onClick function between re-renders
const fetch = useCallback(() => {
console.log('fetch some data here');
}, []);
useEffect(() => {
// this will be triggered only when "fetch" value actually changes
fetch();
}, [fetch]);
// the rest of the code
};
這里要注意,useMemo
和useCallback
只有在重新渲染期間才有用,在初始渲染時,他們的使用會導致react做更多的事情,所以程序也會更慢一些。如果你的代碼到處都是用了成百上千個hook,這種減速甚至是肉眼可感知的。
為什么Component會重新渲染
兩種場景:
- 當Component的state或者prop發生變化時,React會重新渲染該Component
- 當父Component被重新渲染時
即當一個Component重新渲染它自己時,它也會重新渲染它所有的子Component,例如:
const App = () => {
const [state, setState] = useState(1);
return (
<div className="App">
<button onClick={() => setState(state + 1)}> click to re-render {state}</button>
<br />
<Page />
</div>
);
};
App
Component有一些state也有一些自Component,例如 Page
,當點擊頁面上的button,state會發生改變,因此會觸發 App的重新渲染,因此會觸發其所有子Component的重新渲染,包括 Page
,即使他沒有props。
而如果在 Page
內部,還有一些其他子Component:
const Page = () => <Item />;
即使該子Component沒有state也沒有props,但也會由于App
的重新渲染而被觸發重新渲染。由此得出結論,App
由于其state變化而觸發的重新渲染,會觸發整個程序的重新渲染鏈。
如何中斷這條重新渲染鏈呢?對子Component進行緩存:
- 使用
useMemo
hook - 使用
React.memo
工具
只有通過上面兩種方法,React才會在重新渲染之前停止并檢查其props值是否改變:
const Page = () => <Item />;
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
return (
... // same code as before
<PageMemoized />
);
};
有且只有在上述情況下,討論props是否被緩存才有意義。
例如:
const App = () => {
const [state, setState] = useState(1);
const onClick = () => {
console.log('Do something on click');
};
return (
// page will re-render regardless of whether onClick is memoized or not
<Page onClick={onClick} />
);
};
如果 Page
沒有被緩存,當 App
重新渲染時,React發現 Page
是其子Component,就會也重新渲染它。那么不論 onClick
是否使用useCallback都是沒有意義的;
對上述例子進一步優化,緩存 Page
:
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
const onClick = () => {
console.log('Do something on click');
};
return (
// PageMemoized WILL re-render because onClick is not memoized
<PageMemoized onClick={onClick} />
);
};
如果 Page
被緩存,當 App
重新渲染時,React發現 PageMemoized
是其子Component且已使用 React.memo
,因此中斷重新渲染鏈條,首先檢查 PageMemoized
的 props 是否發生變化。上述例子中,由于 onClick
沒有被緩存,所以props發生變化,PageMemoized
會被重新渲染;
對上述例子繼續優化,緩存 Page
,使用 useCallback
:
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
const onClick = useCallback(() => {
console.log('Do something on click');
}, []);
return (
// PageMemoized will NOT re-render because onClick is memoized
<PageMemoized onClick={onClick} />
);
};
那么,React在PageMemoized
上停止重新渲染鏈并檢查其props,發現 onClick
沒有發生變化,因此PageMemoized
也不會被重新渲染;
對上述例子繼續變種,在PageMemoized
上增加另一個沒有緩存的值:
const PageMemoized = React.memo(Page);
const App = () => {
const [state, setState] = useState(1);
const onClick = useCallback(() => {
console.log('Do something on click');
}, []);
return (
// page WILL re-render because value is not memoized
<PageMemoized onClick={onClick} value={[1, 2, 3]} />
);
};
那么,React在PageMemoized
上停止重新渲染鏈并檢查其props,發現 onClick
沒有發生變化,但是value
發生變化,因此PageMemoized
會被重新渲染;
綜上所述,得出結論:只有Component本身和它的每一個props都被緩存時,hook的優化才有意義。否則都是對內存的浪費,且會降低代碼的可讀性。