它是用于react渲染过程 中的性能优化。useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
const cachedValue = useMemo(calculateValue, dependencies)
适用于: 父组件要进行更新,子组件的重新render计算量比较大,而且结果可以复用。就可以使用useMemo来提升父组件引起子组件不必要渲染的性能优化。
useMemo在项目中一定是不得已用才使用(已经出现了明显的问题)。
- useMemo本身有性能消耗,缓存消耗内存,useMemo自身状态的维护也是有性能开销的
- useMemo会增加开发成本,代码变的很复杂不好维护
- react官方在未来会取消useMemo这个钩子
import React, { useState, useMemo } from 'react';
function getCount (count) {
console.log('执行昂贵的计算');
console.time('计算耗时');
let result = 0;
for (let i = 0; i < count * 1000000000; i++) {
result += i;
}
console.timeEnd('计算耗时');
return result;
}
const ExampleComponent = () => {
const [count, setCount] = useState(0);
console.log('ExampleComponent render');
// const expensiveCalculation = getCount(count);
const expensiveCalculation = useMemo(() => {
console.log('执行昂贵的计算');
console.time('计算耗时');
let result = 0;
for (let i = 0; i < count * 100000000; i++) {
result += i;
}
console.timeEnd('计算耗时');
return result;
}, [count]);//代码执行依赖于count值的变化
const incrementCount = () => {
setCount(prevCount => prevCount + 1);
};
const dec = () => {
setCount(prevCount => prevCount - 1);
}
return (
<div>
<p>Count: {count}</p>
<p>Expensive Calculation: {expensiveCalculation}</p>
<button onClick={incrementCount}>Increment</button>
<button onClick={dec}>dec</button>
</div>
);
};
function App() {
const [count, setCount] = useState(0);
return (
<div className="App">
<button onClick={() => setCount(count + 1)}>加加+++</button>
{count}
<ExampleComponent />
</div>
);
}
export default App;
-
[参考]
- [
useMemo(calculateValue, dependencies)]
- [
-
[用法]
- [跳过代价昂贵的重新计算]
- [跳过组件的重新渲染]
- [记忆另一个 Hook 的依赖]
- [记忆一个函数]
-
[故障排除]
- [每次重新渲染时计算函数都会运行两次]
- [我调用的
useMemo应该返回一个对象,但返回了undefined] - [组件每次渲染时,
useMemo都会重新计算] - [我需要为循环中的每个列表项调用
useMemo,但这是不允许的]
参考
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,并返回最新结果。
注意
useMemo是一个 React Hook,所以你只能 在组件的顶层 或者自定义 Hook 中调用它。你不能在循环语句或条件语句中调用它。如有需要,将其提取为一个新组件并使用 state。- 在严格模式下,为了 帮你发现意外的错误,React 将会 调用你的计算函数两次。这只是一个开发环境下的行为,并不会影响到生产环境。如果计算函数是一个纯函数(它本来就应该是),这将不会影响到代码逻辑。其中一次的调用结果将被忽略。
- 除非有特定原因,React 不会丢弃缓存值。例如,在开发过程中,React 会在你编辑组件文件时丢弃缓存。无论是在开发环境还是在生产环境,如果你的组件在初始挂载期间被终止,React 都会丢弃缓存。在未来,React 可能会添加更多利用丢弃缓存的特性——例如,如果 React 在未来增加了对虚拟化列表的内置支持,那么丢弃那些滚出虚拟化列表视口的缓存是有意义的。你可以仅仅依赖
useMemo作为性能优化手段。否则,使用 state 变量 或者 ref 可能更加合适。
注意
这种缓存返回值的方式也叫做 记忆化(memoization),这也是该 Hook 叫做 useMemo 的原因。
用法
跳过代价昂贵的重新计算
在组件顶层调用 useMemo 以在重新渲染之间缓存计算结果:
import { useMemo } from 'react';
function TodoList({ todos, tab, theme }) {
const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
// ...
}
你需要给 useMemo 传递两样东西:
- 一个没有任何参数的 calculation 函数,像这样
() =>,并且返回任何你想要的计算结果。 - 一个由包含在你的组件中并在 calculation 中使用的所有值组成的 依赖列表。
在初次渲染时,你从 useMemo 得到的 值 将会是你的 calculation 函数执行的结果。
在随后的每一次渲染中,React 将会比较前后两次渲染中的 所有依赖项 是否相同。如果通过 Object.is 比较所有依赖项都没有发生变化,那么 useMemo 将会返回之前已经计算过的那个值。否则,React 将会重新执行 calculation 函数并且返回一个新的值。
换言之,useMemo 在多次重新渲染中缓存了 calculation 函数计算的结果直到依赖项的值发生变化。
让我们通过一个示例来看看这在什么情况下是有用的。
默认情况下,React 会在每次重新渲染时重新运行整个组件。例如,如果 TodoList 更新了 state 或从父组件接收到新的 props,filterTodos 函数将会重新运行:
function TodoList({ todos, tab, theme }) {
const visibleTodos = filterTodos(todos, tab);
// ...
}
如果计算速度很快,这将不会产生问题。但是,当正在过滤转换一个大型数组,或者进行一些昂贵的计算,而数据没有改变,那么可能希望跳过这些重复计算。如果 todos 与 tab 都与上次渲染时相同,那么像之前那样将计算函数包装在 useMemo 中,便可以重用已经计算过的 visibleTodos。
这种缓存行为叫做 记忆化。
注意
你应该仅仅把 useMemo 作为性能优化的手段。如果没有它,你的代码就不能正常工作,那么请先找到潜在的问题并修复它。然后再添加 useMemo 以提高性能。
第 1 个示例 共 2 个挑战: 使用 useMemo 跳过重复计算
在这个例子中,filterTodos 的执行被 人为减速了,这样就可以看到渲染期间调用的某些函数确实很慢时会发生什么。尝试切换选项卡并切换主题。
切换选项卡会感觉很慢,因为它迫使减速的 filterTodos 重新执行。这是预料之中的效果,因为“选项卡”已更改,因此整个计算 需要 重新运行。如果你好奇为什么它会运行两次,此处 对此进行了解释。
然后试试切换主题。在 useMemo 的帮助下,尽管已经被人为减速,但是它还是很快!缓慢的 filterTodos 调用被跳过,因为 todos 和 tab(你将其作为依赖项传递给 useMemo)自上次渲染以来都没有改变。
//app.js
import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';
const todos = createTodos();
export default function App() {
const [tab, setTab] = useState('all');
const [isDark, setIsDark] = useState(false);
return (
<>
<button onClick={() => setTab('all')}>
All
</button>
<button onClick={() => setTab('active')}>
Active
</button>
<button onClick={() => setTab('completed')}>
Completed
</button>
<br />
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark mode
</label>
<hr />
<TodoList
todos={todos}
tab={tab}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
//TodoList.js
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>
);
}
//utils.js
export function createTodos() {
const todos = [];
for (let i = 0; i < 50; i++) {
todos.push({
id: i,
text: "Todo " + (i + 1),
completed: Math.random() > 0.5
});
}
return todos;
}
export function filterTodos(todos, tab) {
console.log('[ARTIFICIALLY SLOW] Filtering ' + todos.length + ' todos for "' + tab + '" tab.');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// 在 500 毫秒内不执行任何操作以模拟极慢的代码
}
return todos.filter(todo => {
if (tab === 'all') {
return true;
} else if (tab === 'active') {
return !todo.completed;
} else if (tab === 'completed') {
return todo.completed;
}
});
}
跳过组件的重新渲染
在某些情况下,useMemo 还可以帮助你优化重新渲染子组件的性能。为了说明这一点,假设这个 TodoList 组件将 visibleTodos 作为 props 传递给子 List 组件:
export default function TodoList({ todos, tab, theme }) {
// ...
return (
<div className={theme}>
<List items={visibleTodos} />
</div>
);
}
你已经注意到切换 theme 属性会使应用程序冻结片刻,但是如果你从 JSX 中删除 <List />,感觉会很快。这说明尝试优化 List 组件是值得的。
默认情况下,当一个组件重新渲染时,React 会递归地重新渲染它的所有子组件。这就是为什么当 TodoList 使用不同的 theme 重新渲染时,List 组件 也会 重新渲染。这对于不需要太多计算来重新渲染的组件来说很好。但是如果你已经确认重新渲染很慢,你可以通过将它包装在 memo 中,这样当它的 props 跟上一次渲染相同的时候它就会跳过本次渲染:
import { memo } from 'react';
const List = memo(function List({ items }) {
// ...
});
通过此更改,如果 List 的所有 props 都与上次渲染时相同,则 List 将跳过重新渲染。这就是缓存计算变得重要的地方!想象一下,你在没有 useMemo 的情况下计算了 visibleTodos:
export default function TodoList({ todos, tab, theme }) {
// 每当主题发生变化时,这将是一个不同的数组……
const visibleTodos = filterTodos(todos, tab);
return (
<div className={theme}>
{/* ... 所以List的props永远不会一样,每次都会重新渲染 */}
<List items={visibleTodos} />
</div>
);
}
在上面的示例中,filterTodos 函数总是创建一个不同数组,类似于 {} 总是创建一个新对象的方式。通常,这不是问题,但这意味着 List 属性永远不会相同,并且你的 memo 优化将不起作用。这就是 useMemo 派上用场的地方:
export default function TodoList({ todos, tab, theme }) {
// 告诉 React 在重新渲染之间缓存你的计算结果...
const visibleTodos = useMemo(
() => filterTodos(todos, tab),
[todos, tab] // ...所以只要这些依赖项不变...
);
return (
<div className={theme}>
{/* ... List 也就会接受到相同的 props 并且会跳过重新渲染 */}
<List items={visibleTodos} />
</div>
);
}
通过将 visibleTodos 的计算函数包裹在 useMemo 中,你可以确保它在重新渲染之间具有相同值,直到依赖项发生变化。你 不必 将计算函数包裹在 useMemo 中,除非你出于某些特定原因这样做。在此示例中,这样做的原因是你将它传递给包裹在 memo 中的组件,这使得它可以跳过重新渲染。添加 useMemo 的其他一些原因将在本页进一步描述。
第 1 个示例 共 2 个挑战: 用 useMemo 和 memo 跳过重新渲染
在此示例中,List 组件被 人为地减速了,以便可以看到当渲染的 React 组件真正变慢时会发生什么。尝试切换选项卡并切换主题。
切换选项卡感觉很慢,因为它迫使减速的 List 重新渲染。这是预料之中的,因为选项卡 tab 已更改,因此你需要在屏幕上展示用户的新选择。
接下来,尝试切换主题。感谢 useMemo 和 memo,尽管被人为减速了,但是它还是很快!由于作为依赖性传递给 useMemo 的 todos 与 tab 都没有发生改变,因此 visibleTodos 不会发生改变。由于 visibleTodos 数组从上一次渲染之后就没有发生改变,所以 List 会跳过重新渲染。
import { useState } from 'react';
import { createTodos } from './utils.js';
import TodoList from './TodoList.js';
const todos = createTodos();
export default function App() {
const [tab, setTab] = useState('all');
const [isDark, setIsDark] = useState(false);
return (
<>
<button onClick={() => setTab('all')}>
All
</button>
<button onClick={() => setTab('active')}>
Active
</button>
<button onClick={() => setTab('completed')}>
Completed
</button>
<br />
<label>
<input
type="checkbox"
checked={isDark}
onChange={e => setIsDark(e.target.checked)}
/>
Dark mode
</label>
<hr />
<TodoList
todos={todos}
tab={tab}
theme={isDark ? 'dark' : 'light'}
/>
</>
);
}
//TodoList.js
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>
);
}
//list.js
import { memo } from 'react';
const List = memo(function List({ items }) {
console.log('[ARTIFICIALLY SLOW] Rendering <List /> with ' + items.length + ' items');
let startTime = performance.now();
while (performance.now() - startTime < 500) {
// 在 500 毫秒内不执行任何操作以模拟极慢的代码
}
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.completed ?
<s>{item.text}</s> :
item.text
}
</li>
))}
</ul>
);
});
export default List;
//utils.js
export function createTodos() {
const todos = [];
for (let i = 0; i < 50; i++) {
todos.push({
id: i,
text: "Todo " + (i + 1),
completed: Math.random() > 0.5
});
}
return todos;
}
export function filterTodos(todos, tab) {
return todos.filter(todo => {
if (tab === 'all') {
return true;
} else if (tab === 'active') {
return !todo.completed;
} else if (tab === 'completed') {
return todo.completed;
}
});
}
记忆另一个 Hook 的依赖
假设你有一个计算函数依赖于直接在组件主体中创建的对象:
function Dropdown({ allItems, text }) {
const searchOptions = { matchMode: 'whole-word', text };
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // 🚩 提醒:依赖于在组件主体中创建的对象
// ...
依赖这样的对象会破坏记忆化。当组件重新渲染时,组件主体内的所有代码都会再次运行。创建 searchOptions 对象的代码行也将在每次重新渲染时运行。因为 searchOptions 是你的 useMemo 调用的依赖项,而且每次都不一样,React 知道依赖项是不同的,并且每次都重新计算 searchItems。
要解决此问题,你可以在将其作为依赖项传递之前记忆 searchOptions 对象 本身:
function Dropdown({ allItems, text }) {
const searchOptions = useMemo(() => {
return { matchMode: 'whole-word', text };
}, [text]); // ✅ 只有当 text 改变时才会发生改变
const visibleItems = useMemo(() => {
return searchItems(allItems, searchOptions);
}, [allItems, searchOptions]); // ✅ 只有当 allItems 或 serachOptions 改变时才会发生改变
// ...
在上面的例子中,如果 text 没有改变,searchOptions 对象也不会改变。然而,更好的解决方法是将 searchOptions 对象声明移到 useMemo 计算函数的 内部:
function Dropdown({ allItems, text }) {
const visibleItems = useMemo(() => {
const searchOptions = { matchMode: 'whole-word', text };
return searchItems(allItems, searchOptions);
}, [allItems, text]); // ✅ 只有当 allItems 或者 text 改变的时候才会重新计算
// ...
现在你的计算直接取决于 text(这是一个字符串,不会“意外地”变得不同)。
记忆一个函数
假设 Form 组件被包裹在 memo 中,你想将一个函数作为 props 传递给它:
export default function ProductPage({ productId, referrer }) {
function handleSubmit(orderDetails) {
post('/product/' + productId + '/buy', {
referrer,
orderDetails
});
}
return <Form onSubmit={handleSubmit} />;
}
正如 {} 每次都会创建不同的对象一样,像 function() {} 这样的函数声明和像 () => {} 这样的表达式在每次重新渲染时都会产生一个 不同 的函数。就其本身而言,创建一个新函数不是问题。这不是可以避免的事情!但是,如果 Form 组件被记忆了,大概你想在没有 props 改变时跳过它的重新渲染。总是 不同的 props 会破坏你的记忆化。
要使用 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 }) {
// 此组件函数将为每个渲染运行两次。
const visibleTodos = useMemo(() => {
// 如果任何依赖项发生更改,此计算将运行两次。
return filterTodos(todos, tab);
}, [todos, tab]);
// ...
这是符合预期的,不应对你的代码逻辑产生影响。
这种 仅限开发环境下的 行为可帮助你 保持组件纯粹。React 使用其中一次调用的结果,而忽略另一次的结果。只要你的组件和计算函数是纯函数,这就不会影响你的逻辑。但是,如果你不小心写出带有副作用的代码,这可以帮助你发现并纠正错误。
例如,这个不纯的计算函数会改变你作为 props 收到的数组:
const visibleTodos = useMemo(() => {
// 🚩 错误:改变了 props
todos.push({ id: 'last', text: 'Go for a walk!' });
const filtered = filterTodos(todos, tab);
return filtered;
}, [todos, tab]);
React 调用你的函数两次,所以你会注意到 todo 被添加了两次。你的计算不应更改任何现有对象,但可以更改你在计算期间创建的任何 新 对象。例如,如果 filterTodos 函数总是返回一个 不同 数组,你可以改为改变 那个 数组:
const visibleTodos = useMemo(() => {
const filtered = filterTodos(todos, tab);
// ✅ 正确:改变在计算过程中创建的对象
filtered.push({ id: 'last', text: 'Go for a walk!' });
return filtered;
}, [todos, tab]);
阅读 保持组件纯粹 以了解有关纯组件的更多信息。
此外,请查看有关不通过对象或者数组的可变性直接 更新对象 和 更新数组 的指南。
我调用的 useMemo 应该返回一个对象,但返回了 undefined
这段代码不起作用:
// 🔴 你不能像这样 `() => {` 在箭头函数中直接返回一个对象
const searchOptions = useMemo(() => {
matchMode: 'whole-word',
text: text
}, [text]);
在 JavaScript 中,() => { 是箭头函数体的开始标志,因此 { 大括号不是对象的一部分。这就是它不返回对象并导致错误的原因。你可以通过添加像 ({ 与 }) 这样的括号来修复它:
// 这行得通,但很容易有人再次破坏
const searchOptions = useMemo(() => ({
matchMode: 'whole-word',
text: text
}), [text]);
然而,这仍然令人困惑,而且对于某些人来说,通过移除括号来破坏它太容易了。
为避免此错误,请显式编写 return 语句:
// ✅ 这有效并且是明确的
const searchOptions = useMemo(() => {
return {
matchMode: 'whole-word',
text: text
};
}, [text]);
组件每次渲染时,useMemo 都会重新计算
确保你已将依赖项数组指定为第二个参数!
如果你忘记了依赖数组,useMemo 将每次重新运行计算:
function TodoList({ todos, tab }) {
// 🔴 每次都重新计算:没有依赖数组
const visibleTodos = useMemo(() => filterTodos(todos, tab));
// ...
这是将依赖项数组作为第二个参数传递的更正版本:
function TodoList({ todos, tab }) {
// ✅ 不会不必要地重新计算
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]); // 数组之间的第一个依赖项是否相同?
Object.is(temp1[1], temp2[1]); // 数组之间的第二个依赖项是否相同?
Object.is(temp1[2], temp2[2]); // ... 依此类推 ...
当你发现是哪个依赖项破坏了记忆化时,要么找到一种方法将其删除,要么 也对其进行记忆化。
我需要为循环中的每个列表项调用 useMemo,但这是不允许的
假设 Chart 组件被包裹在 memo 中。当 ReportList 组件重新渲染时,你想跳过重新渲染列表中的每个 Chart。但是,你不能在循环中调用 useMemo:
function ReportList({ items }) {
return (
<article>
{items.map(item => {
// 🔴 你不能像这样在循环中调用 useMemo:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure key={item.id}>
<Chart data={data} />
</figure>
);
})}
</article>
);
}
相反,为每个 item 提取一个组件并为单个 item 记忆数据:
function ReportList({ items }) {
return (
<article>
{items.map(item =>
<Report key={item.id} item={item} />
)}
</article>
);
}
function Report({ item }) {
// ✅ 在顶层调用 useMemo:
const data = useMemo(() => calculateReport(item), [item]);
return (
<figure>
<Chart data={data} />
</figure>
);
}
或者,你可以删除 useMemo 并将 Report 本身包装在 memo 中。如果 item props 没有改变,Report 将跳过重新渲染,因此 Chart 也会跳过重新渲染:
function ReportList({ items }) {
// ...
}
const Report = memo(function Report({ item }) {
const data = calculateReport(item);
return (
<figure>
<Chart data={data} />
</figure>
);
});