useMemo
是一個 React Hook,可讓您在重新渲染之間快取計算結果。
const cachedValue = useMemo(calculateValue, dependencies)
參考
useMemo(calculateValue, dependencies)
在元件的頂層呼叫 useMemo
以快取重新渲染之間的計算結果
import { useMemo } from 'react';
function TodoList({ todos, tab }) {
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab]
);
// ...
}
參數
-
calculateValue
:計算您想要快取的值的函式。它應該是純粹的,不應該接受任何參數,並且應該返回任何類型的值。 React 將在初始渲染期間呼叫您的函式。在下一次渲染時,如果自上次渲染以來dependencies
沒有更改,React 將再次返回相同的值。否則,它將呼叫calculateValue
,返回其結果,並將其儲存以便稍後重複使用。 -
dependencies
:calculateValue
程式碼內參考的所有反應值的清單。反應值包括 props、state 以及直接在元件主體內宣告的所有變數和函式。如果您的程式碼檢查器已 針對 React 進行設定,它將驗證每個反應值是否已正確指定為依賴項。依賴項清單必須具有固定數量的項目,並且像[dep1, dep2, dep3]
一樣內嵌編寫。 React 將使用Object.is
比較將每個依賴項与其先前的值進行比較。
回傳值
在初始渲染時,useMemo
回傳呼叫 calculateValue
且不帶參數的結果。
在後續渲染期間,它將回傳上次渲染中已儲存的值(如果依賴項未更改),或者再次呼叫 calculateValue
並回傳 calculateValue
回傳的結果。
注意事項
範例 1說明 2: 使用 useMemo
跳過重新計算
在此範例中,filterTodos
的實作被**人為地減慢**,以便你可以看到在渲染期間呼叫某些 JavaScript 函式時,如果它真的很慢會發生什麼事。嘗試切換分頁和切換主題。
切換分頁感覺很慢,因為它會強制重新執行被減慢的 filterTodos
。這是預期的,因為 tab
已經改變,因此整個計算*需要*重新執行。(如果你好奇為什麼它會執行兩次,這裡有說明。)
切換主題。**由於使用了 useMemo
,即使人為減慢速度,它仍然很快!** 緩慢的 filterTodos
呼叫被跳過了,因為 todos
和 tab
(你將它們作為依賴項傳遞給 useMemo
)自上次渲染以來都沒有改變。
import { useMemo } from 'react'; import { filterTodos } from './utils.js' export default function TodoList({ todos, theme, tab }) { const visibleTodos = useMemo( () => filterTodos(todos, tab), [todos, tab] ); return ( <div className={theme}> <p><b>Note: <code>filterTodos</code> is artificially slowed down!</b></p> <ul> {visibleTodos.map(todo => ( <li key={todo.id}> {todo.completed ? <s>{todo.text}</s> : todo.text } </li> ))} </ul> </div> ); }
跳過組件的重新渲染
在某些情況下,useMemo
也可以幫助你優化子組件重新渲染的效能。為了說明這一點,假設這個 TodoList
組件將 visibleTodos
作為 prop 傳遞給子組件 List
組件
export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
}
你可能注意到切換 theme
屬性會讓應用程式凍結一下,但如果從 JSX 中移除 <List />
,就會感覺很快。這告訴你值得嘗試優化 List
元件。
預設情況下,當一個元件重新渲染時,React 會遞迴地重新渲染其所有子元件。這就是為什麼當 TodoList
使用不同的 theme
重新渲染時,List
元件*也會*重新渲染。對於不需要太多計算即可重新渲染的元件來說,這樣沒問題。但是,如果你已驗證重新渲染速度很慢,則可以透過將 List
包裝在 memo
中,來告訴 List
當其屬性與上次渲染時相同則跳過重新渲染:
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
透過這個更改,如果 List
的所有屬性都與上次渲染時*相同*,它將會跳過重新渲染。這就是快取計算變得重要的地方!想像一下,你在沒有使用 useMemo
的情況下計算了 visibleTodos
export default function TodoList({ todos, tab, theme }) {
// Every time the theme changes, this will be a different array...
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>
{/* ... so List's props will never be the same, and it will re-render every time */}
<List items={visibleTodos} />
</div>
);
}
在上面的例子中,filterTodos
函式總是會建立一個*不同的*陣列,類似於 {}
物件字面量總是會建立一個新的物件。通常,這不會造成問題,但這表示 List
屬性永遠不會相同,並且你的 memo
優化將無法作用。這就是 useMemo
派上用場的地方
export default function TodoList({ todos, tab, theme }) {
// Tell React to cache your calculation between re-renders...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...so as long as these dependencies don't change...
);
return (
<div className={theme}>
{/* ...List will receive the same props and can skip re-rendering */}
<List items={visibleTodos} />
</div>
);
}
透過將 visibleTodos
計算包裝在 useMemo
中,你可以確保它在重新渲染之間具有*相同*的值(直到依賴項發生變化)。除非你有特殊原因,否則你*不必*將計算包裝在 useMemo
中。在此範例中,原因是你將它傳遞給包裝在 memo
中的元件,這讓它可以跳過重新渲染。還有其他一些新增 useMemo
的原因,在本頁面稍後會說明。
深入探討
你可以將 <List />
JSX 節點本身包裝在 useMemo
中,而不是將 List
包裝在 memo
中
export default function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
const children = useMemo(() => <List items={visibleTodos} />, [visibleTodos]);
return (
<div className={theme}>
{children}
</div>
);
}
行為將會相同。如果 visibleTodos
沒有更改,則 List
將不會重新渲染。
像 <List items={visibleTodos} />
這樣的 JSX 節點是一個像 { type: List, props: { items: visibleTodos } }
的物件。建立這個物件的成本很低,但 React 不知道其內容是否與上次相同。這就是為什麼預設情況下,React 會重新渲染 List
元件。
但是,如果 React 看到與先前渲染期間完全相同的 JSX,它將不會嘗試重新渲染你的元件。這是因為 JSX 節點是不可變的。JSX 節點物件不會隨著時間改變,因此 React 知道可以安全地跳過重新渲染。但是,要使其運作,節點必須*實際上是同一個物件*,而不僅僅是在程式碼中看起來相同。這就是 useMemo
在此範例中所做的事情。
手動將 JSX 節點包裝到 useMemo
中並不方便。例如,你無法有條件地執行此操作。這就是為什麼你通常會使用 memo
包裝元件,而不是包裝 JSX 節點的原因。
範例 1說明 2: 使用 useMemo
和 memo
跳過重新渲染
在此範例中,List
元件被人為地減慢速度,以便你可以看到當你正在渲染的 React 元件確實很慢時會發生什麼情況。嘗試切換分頁和主題。
切換分頁感覺很慢,因為它會強制減慢速度的 List
重新渲染。這是預期之內的,因為 tab
已更改,因此你需要在螢幕上反映使用者新的選擇。
接下來,嘗試切換主題。由於使用了 useMemo
以及 memo
,即使人為地降低速度,它仍然很快! List
跳過了重新渲染,因為自上次渲染以來,visibleTodos
陣列沒有改變。 visibleTodos
陣列沒有改變,因為 todos
和 tab
(您將它們作為依賴項傳遞給 useMemo
)自上次渲染以來都沒有改變。
import { useMemo } from 'react'; import List from './List.js'; import { filterTodos } from './utils.js' export default function TodoList({ todos, theme, tab }) { const visibleTodos = useMemo( () => filterTodos(todos, tab), [todos, tab] ); return ( <div className={theme}> <p><b>Note: <code>List</code> is artificially slowed down!</b></p> <List items={visibleTodos} /> </div> ); }
防止 Effect 過於頻繁地觸發
有時,您可能希望在 Effect 內使用一個值:
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = {
serverUrl: 'https://127.0.0.1:1234',
roomId: roomId
}
useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...
這會產生一個問題。 每個反應值都必須聲明為 Effect 的依賴項。 但是,如果您將 options
宣告為依賴項,它將導致您的 Effect 不斷重新連接到聊天室
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // 🔴 Problem: This dependency changes on every render
// ...
要解決此問題,您可以將需要從 Effect 中呼叫的物件包裝在 useMemo
中
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = useMemo(() => {
return {
serverUrl: 'https://127.0.0.1:1234',
roomId: roomId
};
}, [roomId]); // ✅ Only changes when roomId changes
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ Only changes when createOptions changes
// ...
這可確保如果 useMemo
返回快取的物件,則 options
物件在重新渲染之間保持相同。
然而,由於 useMemo
是效能優化,而不是語義保證,如果 有特定原因需要這樣做,React可能會丟棄快取的值。這也會導致 effect 重新觸發,因此更好的做法是通過將您的物件移動到 Effect *內部* 來消除對函數依賴項的需求
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const options = { // ✅ No need for useMemo or object dependencies!
serverUrl: 'https://127.0.0.1:1234',
roomId: roomId
}
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ Only changes when roomId changes
// ...
現在您的程式碼更簡單,而且不需要 useMemo
。 瞭解更多關於移除 Effect 依賴項的資訊。
記憶體化另一個 Hook 的依賴項
假設您有一個計算,它依賴於直接在組件主體中創建的物件
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 Caution: Dependency on an object created in the component body
// ...
依賴像這樣的物件會失去記憶體化的意義。當組件重新渲染時,組件主體內的所有程式碼都會再次運行。創建 searchOptions
物件的程式碼行也將在每次重新渲染時運行。 由於 searchOptions
是您的 useMemo
呼叫的依賴項,而且它每次都不同,React 知道依賴項不同,並且每次都會重新計算 searchItems
。
要解決此問題,您可以在將其作為依賴項傳遞之前,先將 searchOptions
物件*本身*記憶體化
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ Only changes when text changes
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ Only changes when allItems or searchOptions changes
// ...
在上面的範例中,如果 text
沒有改變,則 searchOptions
物件也不會改變。但是,更好的解決方案是將 searchOptions
物件宣告移動到 useMemo
計算函數的*內部*
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ Only changes when allItems or text changes
// ...
現在您的計算直接依賴於 text
(它是一個字串,不會“意外地”變得不同)。
記憶體化函數
假設 Form
組件被 memo
包裹。您想要將一個函數作為 prop 傳遞給它
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
}
就像 {}
創建一個不同的物件一樣,函數宣告,例如 function() {}
和表達式,例如 () => {}
,在每次重新渲染時都會產生一個*不同*的函數。就其本身而言,創建一個新函數並不是問題。這不是需要避免的事情!但是,如果 Form
組件被記憶體化,則假設您希望在沒有 prop 改變時跳過重新渲染它。一個*總是*不同的 prop 會失去記憶體化的意義。
要使用 useMemo
記憶體化函數,您的計算函數必須返回另一個函數
export default function Page({ productId, referrer }) {
const handleSubmit = useMemo(() => {
return (orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
};
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
這看起來很笨拙!記憶體化函數很常見,以至於 React 有一個專門用於此的內建 Hook。將您的函數包裝到 useCallback
中,而不是 useMemo
中,以避免必須編寫額外的巢狀函數
export default function Page({ productId, referrer }) {
const handleSubmit = useCallback((orderDetails) => {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}, [productId, referrer]);
return <Form onSubmit={handleSubmit} />;
}
上面兩個範例完全相同。 useCallback
的唯一好處是它可以讓您避免在內部編寫額外的巢狀函數。它沒有做任何其他事情。 閱讀更多關於 useCallback
的資訊。
疑難排解
我的計算在每次重新渲染時都會執行兩次
在嚴格模式中,React 會呼叫您的某些函式兩次而不是一次。
function TodoList({ todos, tab }) {
// This component function will run twice for every render.
const visibleTodos = useMemo(() => {
// This calculation will run twice if any of the dependencies change.
return filterTodos(todos, tab);
}, [todos, tab]);
// ...
這是預期的行為,而且不應該會破壞您的程式碼。
這種僅限開發環境的行為可以幫助您保持元件的純粹性。React 會使用其中一次呼叫的結果,並忽略另一次呼叫的結果。只要您的元件和計算函式是純粹的,這就不應該會影響您的邏輯。但是,如果它們不小心不純粹,這可以幫助您注意到並修正錯誤。
例如,這個不純粹的計算函式會改變您作為 prop 接收的陣列
const visibleTodos = useMemo(() => {
// 🚩 Mistake: mutating a prop
todos.push({ id: 'last', text: 'Go for a walk!' });
const filtered = filterTodos(todos, tab);
return filtered;
}, [todos, tab]);
React 會呼叫您的函式兩次,因此您會注意到待辦事項被添加了兩次。您的計算不應該更改任何現有的物件,但可以更改您在計算過程中建立的任何*新*物件。例如,如果 filterTodos
函式總是返回一個*不同*的陣列,您可以改為改變*那個*陣列
const visibleTodos = useMemo(() => {
const filtered = filterTodos(todos, tab);
// ✅ Correct: mutating an object you created during the calculation
filtered.push({ id: 'last', text: 'Go for a walk!' });
return filtered;
}, [todos, tab]);
閱讀保持元件的純粹性以瞭解更多關於純粹性的資訊。
我的 useMemo
呼叫應該返回一個物件,但返回 undefined
這段程式碼無法運作
// 🔴 You can't return an object from an arrow function with () => {
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
text: text
}, [text]);
在 JavaScript 中,() => {
開始箭頭函式的函式主體,所以 {
大括號不是您物件的一部分。這就是它沒有返回物件的原因,並且導致錯誤。您可以透過添加括號來修復它,例如 ({
和 })
// This works, but is easy for someone to break again
const searchOptions = useMemo(() => ({
matchMode: 'whole-word',
text: text
}), [text]);
然而,這仍然令人困惑,而且很容易讓其他人透過移除括號來破壞它。
為了避免這個錯誤,請明確地寫一個 return
陳述式
// ✅ This works and is explicit
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]);
每次我的元件渲染時,useMemo
中的計算都會重新執行
請確保您已將依賴陣列指定為第二個參數!
如果您忘記依賴陣列,useMemo
每次都會重新執行計算
function TodoList({ todos, tab }) {
// 🔴 Recalculates every time: no dependency array
const visibleTodos = useMemo(() => filterTodos(todos, tab));
// ...
這是將依賴陣列作為第二個參數傳遞的修正版本
function TodoList({ todos, tab }) {
// ✅ Does not recalculate unnecessarily
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
如果這沒有幫助,則問題是至少有一個依賴項與先前的渲染不同。您可以透過手動將您的依賴項記錄到主控台來除錯此問題
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
console.log([todos, tab]);
然後,您可以在主控台中右鍵點擊來自不同重新渲染的陣列,並為它們選擇「儲存為全域變數」。假設第一個被儲存為 temp1
,第二個被儲存為 temp2
,那麼您可以使用瀏覽器主控台來檢查兩個陣列中的每個依賴項是否相同
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
當您找到哪個依賴項破壞了記憶體時,請找到移除它的方法,或者也將其記憶體化。
我需要在迴圈中為每個清單項目呼叫 useMemo
,但不允許這樣做
假設 Chart
元件被 memo
包裹。當 ReportList
元件重新渲染時,您想要略過清單中每個 Chart
的重新渲染。但是,您不能在迴圈中呼叫 useMemo
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 You can't call useMemo in a loop like this:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure key={item.id}>
<Chart data={data} />
</figure>
);
})}
</article>
);
}
相反,請為每個項目提取一個元件,並記憶體化個別項目的資料
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ Call useMemo at the top level:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure>
<Chart data={data} />
</figure>
);
}
或者,您可以移除 useMemo
,而是將 Report
本身包裹在 memo
中。如果 item
prop 沒有改變,Report
將會略過重新渲染,因此 Chart
也將會略過重新渲染
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
const data = calculateReport(item);
return (
<figure>
<Chart data={data} />
</figure>
);
});