在這篇文章中將介紹如何在 React 應用程式中使用 useMemo、useCallback、React's memo API 去優化性能,但是需要注意的是不要濫用這些 API。
不要將 React 的 useMemo Hook 與 React 的 memo API 混淆了。useMemo 被用於緩存值,memo API 被用於包裹 React 組件去阻止組件重新渲染。
不要將 React 的 useMemo Hook 與 React 的 useCallback Hook 混淆了。useMemo 被用於緩存值,useCallback 被用於緩存函數。
在 React 中怎麼使用 useMemoReact 中的 useMemo 被用於優化 React 函數組件的計算性能。接下來我將會通過一個例子來說明函數組件的性能問題,然後再使用 useMemo 來解決它。
讓我們以下面的 React 應用程式為例,在這個例子中渲染了一個用戶列表,並且我們可以使用用戶名去過濾用戶,在點擊按鈕時才發生過濾,在輸入框中輸入用戶名時不過濾。代碼如下:
import React from 'react';
const users = [
{ id: 'a', name: 'Robin' },
{ id: 'b', name: 'Dennis' },
];
const App = () => {
const [text, setText] = React.useState('');
const [search, setSearch] = React.useState('');
const handleText = (event) => {
setText(event.target.value);
};
const handleSearch = () => {
setSearch(text);
};
const filteredUsers = users.filter((user) => {
return user.name.toLowerCase().includes(search.toLowerCase());
});
return (
<div>
<input type="text" value={text} onChange={handleText} />
<button type="button" onClick={handleSearch}>
Search
</button>
<List list={filteredUsers} />
</div>
);
};
const List = ({ list }) => {
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
};
const ListItem = ({ item }) => {
return <li>{item.name}</li>;
};
export default App;雖然每一次往輸入框中輸入值時 filteredUsers 的值不會發生變化,但是 filter 的回調函數在每一次往輸入框中輸入值時都會執行。我們可以這樣修改代碼:
function App() {
...
const filteredUsers = users.filter((user) => {
console.log('Filter function is running ...');
return user.name.toLowerCase().includes(search.toLowerCase());
});
...
}在這個小的 React 應用程式中這不是什麼大的問題,但是如果數組中有一批大的數據並且在每一次鍵盤輸入時 filter 的回調函數都會執行,我們可能會感覺到應用程式很慢。因此,我們使用 React 的 useMemo Hook 緩存函數的返回值,僅僅在 useMemo 依賴項發生變化的時候函數才會重新運行,從上面的代碼中我們可以看出 filter 的回調函數的依賴項是 search。所以使用 useMemo 代碼如下:
function App() {
...
const filteredUsers = React.useMemo(() => {
return users.filter(user => {
console.log('Filter function is running ...');
return user.name.toLowerCase().includes(search.toLowerCase());
})
},[search])
...
}現在,filter 的回調函數僅僅在search發生變化時才會執行,text的值發生變化時函數不會執行,因為text不是 useMemo 的依賴項。你可以自己嘗試一下,現在往輸入框中輸入值時控制臺上不會出現 console.log 的列印值,僅僅到點擊按鈕時才會有列印。
你可能會想知道為什麼不在所有的值計算中使用 useMemo Hook 或者 React 為什麼不默認給所有的值計算使用 useMemo Hook。這是因為在每一次組件重新渲染時,useMemo Hook 都會比較依賴數組中的每一個依賴項以決定是否要重新計算值,進行依賴項的比較可能比重新計算值更耗費性能。
總結:如果給 useMemo 傳遞了依賴項數組,只有在依賴項發生變化時 useMemo 才會重新計算新的值(如果依賴項數據是個空數組,只在組件第一次渲染時計算值),如果沒有給 useMemo 傳遞依賴數組,在每一次渲染時都會重新計算新的值。
在 React 中怎麼使用 memo API在 React 中可以用 memo API 來優化函數組件的渲染行為。接下來我們將會通過一個例子來說明函數組件的性能問題,然後再使用 memo API 來解決它。
讓我們以下面的React應用程式為例,在這個例子中渲染了一個用戶列表,我們可以往用戶列表中新增用戶。代碼如下:
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
const App = () => {
console.log('Render: App');
const [users, setUsers] = React.useState([
{ id: 'a', name: 'Robin' },
{ id: 'b', name: 'Dennis' },
]);
const [text, setText] = React.useState('');
const handleText = (event) => {
setText(event.target.value);
};
const handleAddUser = () => {
setUsers(users.concat({ id: uuidv4(), name: text }));
};
return (
<div>
<input type="text" value={text} onChange={handleText} />
<button type="button" onClick={handleAddUser}>
Add User
</button>
<List list={users} />
</div>
);
};
const List = ({ list }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
};
const ListItem = ({ item }) => {
console.log('Render: ListItem');
return <li>{item.name}</li>;
};
export default App;你會發現每往輸入框中輸入一個字符,所有的組件都會重新渲染
// 往輸入框中輸入一個字符之後
Render: App
Render: List
Render: ListItem
Render: ListItem在這個小的 React 應用程式中這不會有什麼問題,但是如果這裡是一個大的用戶列表,用戶在往輸入框中輸入字符就會感覺到慢,在這個時候我們可以使用 memo API 進行優化。改寫代碼如下:
const List = React.memo(({ list }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} />
))}
</ul>
);
});
const ListItem = ({ item }) => {
console.log('Render: ListItem');
return <li>{item.name}</li>;
};現在我們往輸入框中輸入字符,僅僅 App 組件會被重新渲染。React.memo 會檢查 List 組件的 props 是否發生變化,如果沒有變化會跳過 List 組件的重新渲染。在這個例子中,往輸入框中輸入字符不會導致 List 組件的 list prop 發生變化,所以 List 不會重新渲染,因而 ListItem 也不會重新渲染。看上去我們不需要給 ListItem 使用 memo API,但是如果往用戶列表中新增一個用戶,在控制臺中你將看到以下輸出:
// 添加一個新的用戶之後
Render: App
Render: List
Render: ListItem
Render: ListItem
Render: ListItem當添加一個新的用戶會導致 List 組件被重新渲染,但是 ListItem 被渲染三次,我們希望只渲染一個新的用戶,而不是所有的用戶,所以使用 React.memo 改寫 ListItem:
const ListItem = React.memo(({ item }) => {
console.log('Render: ListItem');
return <li>{item.name}</li>;
});經過改寫,往用戶列表中添加一個新的用戶,你會看到如下的輸出:
// a添加一個新的用戶之後
Render: App
Render: List
Render: ListItem只有新的 ListItem 被渲染了,用戶列表之前的 ListItem 保持不變。
你可能會想知道為什麼不在所有的組件中使用 memo API 或者 React 為什麼不默認給所有的組件使用 memo API。這是因為 memo API 會將新的 props 與之前的 props 進行比較以決定是否重新渲染組件,進行 props 的比較可能比重新渲染更耗費性能。
總之,當你的 React 組件變慢並且您想要改進它們的性能時,React的 memo API 就會很有用。這通常發生在數據量大的組件中,比如在一個數據點發生變化,許多組件必須重新渲染的巨大列表時。
在 React 中怎麼使用 useCallback在 React 中可以用 useCallback hook 來優化函數組件的渲染行為。接下來我們將會通過一個例子來說明函數組件的性能問題,然後再使用 useCallback 來解決它。
讓我們以下面的React應用程式為例,在這個例子中渲染了一個用戶列表,我們可以往用戶列表中新增用戶也可以從用戶列表中刪除用戶。代碼如下:
import React from 'react';
import { v4 as uuidv4 } from 'uuid';
const App = () => {
console.log('Render: App');
const [users, setUsers] = React.useState([
{ id: 'a', name: 'Robin' },
{ id: 'b', name: 'Dennis' },
]);
const [text, setText] = React.useState('');
const handleText = (event) => {
setText(event.target.value);
};
const handleAddUser = () =>{
setUsers(users.concat({ id: uuidv4(), name: text }));
};
const handleRemove = (id) => {
setUsers(users.filter((user) => user.id !== id));
};
return (
<div>
<input type="text" value={text} onChange={handleText} />
<button type="button" onClick={handleAddUser}>
Add User
</button>
<List list={users} onRemove={handleRemove} />
</div>
);
};
const List = ({ list, onRemove }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} onRemove={onRemove} />
))}
</ul>
);
};
const ListItem = ({ item, onRemove }) => {
console.log('Render: ListItem');
return (
<li>
{item.name}
<button type="button" onClick={() => onRemove(item.id)}>
Remove
</button>
</li>
);
};
export default App;如果你在一個 react 應用中寫入上面的代碼,你會發現每次往輸入框中輸入字符,控制臺都會列印:
Render: App
Render: List
Render: ListItem
Render: ListItem我們希望往輸入框中輸入字符,只是 App 組件重新渲染,App 的子組件不重新渲染。根據上文的介紹,我們使用 memo API 來阻止子組件的更新,於是改寫 List 和 ListItem 組件:
const List = React.memo(({ list, onRemove }) => {
console.log('Render: List');
return (
<ul>
{list.map((item) => (
<ListItem key={item.id} item={item} onRemove={onRemove} />
))}
</ul>
);
})
const ListItem = React.memo(({ item, onRemove }) => {
console.log('Render: ListItem');
return (
<li>
{item.name}
<button type="button" onClick={() => onRemove(item.id)}>
Remove
</button>
</li>
);
});經過改寫之後你發現每往輸入框中輸入一個字符,List 和 ListItem 還是會重新渲染。
讓我們看一下傳遞給 List 組件的 props
const App = () => {
// How we're rendering the List in the App component
return (
//...
<List list={users} onRemove={handleRemove} />
)
}只要沒有從 List 組件中添加或刪除任何項,即使用戶在輸入框中輸入一些內容後就算 App 組件重新渲染,List 也應該保持原樣。因此,罪魁禍首是 onRemove 回調處理程序。
每當用戶在輸入框中輸入一些內容後 App 組件重新渲染,App 組件中的 handleRemove 都會被重新定義。往 List 組件中傳遞一個新的函數作為 prop,List 注意到一個 prop 與之前的渲染相比發生了變化。這就是每當用戶在輸入欄位框中輸入一些內容後 List 和 ListItem 會重新渲染的原因。
現在我們使用 useCallback Hook 改寫代碼:
const App = () => {
...
// 依賴項數組作為 useCallback 的第二個參數
const handleRemove = React.useCallback(
(id) => setUsers(users.filter((user) => user.id !== id)),
[users]
);
...
};依賴項數組中的任何一項發生變化都會導致 handleRemove 被重新定義。如果 users 被改變了,handleRemove 會被重新定義,List 和 ListItem 也應該被重新渲染。用戶在輸入框中輸入一些內容不會導致 handleRemove 被重新定義,它保持原樣。子組件的 prop 不會發生改變,所以不會重新渲染。
你可能會想知道為什麼不在所有的函數中使用 useCallback Hook 或者 React 為什麼不默認給所有的函數使用 useCallback Hook。這是因為在每一次重新渲染時 useCallback Hook 會比較依賴數組中的每一個依賴項以決定是否要重新定義函數,進行依賴項的比較可能比重新渲染更耗費性能。
總之,React 的 useCallback Hook 用於緩存函數。當函數被當作 props 傳遞給其他組件我們不用擔心函數會因為父組件重新渲染而被重新初始化。然而,正如你所看到的,當與 React 的 memo API 一起使用時,React 的 useCallback 鉤子開始發揮作用
總結:如果給 useCallback 傳遞了依賴項數組,只有在依賴項發生變化時 useCallback 才會重新定義函數(如果依賴項數據是個空數組,只在組件第一次渲染時重新定義函數),如果沒有給 useCallback 傳遞依賴數組,在每一次渲染時都會重新定義函數。useCallback(fn, deps) 等同於 useMemo(() => fn, deps)。