跳转到内容

useMemo 与 computed

从 5,000 条商品中筛选、排序并计算合计。修改搜索词或排序方式会重新计算;增加购物车数量与结果无关,两边都会复用缓存。

React

useMemo 根据手动依赖缓存筛选、排序和汇总结果。

在线预览

缓存的计算结果

5,000 件商品

合计 ¥2,738,800

  • 机械键盘 0001¥99
  • 机械键盘 0901¥99
  • 机械键盘 1801¥99
  • 机械键盘 2701¥99
  • 机械键盘 3601¥99

购物车不是依赖,更新它会复用上一次计算结果。

组件关系与数据流

StateComponentCached DerivedData

差异焦点缓存正确性依赖开发者维护完整的依赖数组。

实线表示依赖、数据或命令传递,虚线表示事件、回调或清理路径。

flowchart TB
  accTitle: React useMemo 显式依赖缓存
  accDescr: ReactDemo 每次状态更新都会重新执行,useMemo 手动比较 keyword 和 sortOrder 依赖数组,只有依赖变化才重新计算 result。
  state(["keyword / sortOrder / cartCount"]) --> render["ReactDemo 每次 state 更新都执行"]
  render --> deps[("手写 [keyword, sortOrder]")]
  products[("5,000 products")] --> memo{{"useMemo 计算或复用 result"}}
  deps --> memo
  memo --> ui["结果 UI"]
  class state state
  class render,ui component
  class memo process
  class deps,products resource
  classDef state fill:#ffffff,stroke:#149eca,color:#172033,stroke-width:1.2px
  classDef component fill:#e5f6fb,stroke:#149eca,color:#172033,stroke-width:2px,font-weight:600
  classDef process fill:#f4fbfd,stroke:#149eca,color:#172033,stroke-width:1.6px
  classDef resource fill:#f3f5f8,stroke:#778195,color:#172033,stroke-width:1.4px

组件仍会重新执行;useMemo 通过手写依赖数组决定计算还是复用。

React 流程图
React 组件关系与数据流

滚轮缩放 · 按住拖动 · 双击重置 · Esc 关闭

实际运行源码

import { useMemo, useState } from "react";
type SortOrder = "asc" | "desc";
const categories = ["机械键盘", "无线鼠标", "显示器", "前端书籍"];
const products = Array.from({ length: 5_000 }, (_, index) => ({
id: index + 1,
name: `${categories[index % categories.length]} ${String(index + 1).padStart(4, "0")}`,
price: 99 + ((index * 37) % 900),
}));
const priceFormatter = new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY",
maximumFractionDigits: 0,
});
export function ReactDemo() {
const [keyword, setKeyword] = useState("");
const [sortOrder, setSortOrder] = useState<SortOrder>("asc");
const [cartCount, setCartCount] = useState(0);
const result = useMemo(() => {
const visibleProducts = products
.filter((product) => product.name.includes(keyword.trim()))
.sort((a, b) =>
sortOrder === "asc" ? a.price - b.price : b.price - a.price,
);
return {
count: visibleProducts.length,
total: visibleProducts.reduce((sum, product) => sum + product.price, 0),
preview: visibleProducts.slice(0, 5),
};
}, [keyword, sortOrder]);
return (
<div className="demo-stack">
<div className="demo-form">
<label className="demo-field">
搜索 5,000 条商品
<input
className="demo-input"
value={keyword}
placeholder="例如 机械键盘"
onChange={(event) => setKeyword(event.target.value)}
/>
</label>
<label className="demo-field">
价格排序
<select
className="demo-select"
value={sortOrder}
onChange={(event) => setSortOrder(event.target.value as SortOrder)}
>
<option value="asc">从低到高</option>
<option value="desc">从高到低</option>
</select>
</label>
</div>
<div className="demo-card">
<span className="demo-tag">缓存的计算结果</span>
<h2>{result.count.toLocaleString()} 件商品</h2>
<p>合计 {priceFormatter.format(result.total)}</p>
</div>
<ul className="demo-list">
{result.preview.map((product) => (
<li key={product.id}>
<span>{product.name}</span>
<span>{priceFormatter.format(product.price)}</span>
</li>
))}
</ul>
<div className="demo-actions">
<button
className="demo-button"
type="button"
onClick={() => setCartCount((count) => count + 1)}
>
购物车 +1(当前 {cartCount}
</button>
</div>
<p className="demo-muted">购物车不是依赖,更新它会复用上一次计算结果。</p>
</div>
);
}
Vue 3

computed 自动追踪读取的响应式状态并缓存结果。

在线预览

缓存的计算结果

5,000 件商品

合计 ¥2,738,800

  • 机械键盘 0001¥99
  • 机械键盘 0901¥99
  • 机械键盘 1801¥99
  • 机械键盘 2701¥99
  • 机械键盘 3601¥99

购物车不是依赖,更新它会复用上一次计算结果。

组件关系与数据流

StateComponentCached DerivedData

差异焦点依赖来自运行时响应式读取,不需要手写依赖数组。

实线表示依赖、数据或命令传递,虚线表示事件、回调或清理路径。

flowchart TB
  accTitle: Vue computed 自动依赖缓存
  accDescr: computed 执行时自动记录 keyword 和 sortOrder refs,cartCount 未被读取因此不会使 result 失效,模板读取时复用缓存。
  deps(["keyword + sortOrder refs"]) --> memo{{"computed 自动追踪读取"}}
  products[("5,000 products")] --> memo
  memo --> cache[("缓存 result")]
  cache --> ui["结果 UI"]
  cart(["cartCount ref"]) -->|"独立模板依赖"| ui
  class deps,cart state
  class ui component
  class memo process
  class products,cache resource
  classDef state fill:#ffffff,stroke:#42b883,color:#172033,stroke-width:1.2px
  classDef component fill:#e7f8f0,stroke:#42b883,color:#172033,stroke-width:2px,font-weight:600
  classDef process fill:#f2fbf7,stroke:#42b883,color:#172033,stroke-width:1.6px
  classDef resource fill:#f3f5f8,stroke:#778195,color:#172033,stroke-width:1.4px

computed 自动收集 keyword / sortOrder;未读取 cartCount,因此缓存不失效。

Vue 3 流程图
Vue 3 组件关系与数据流

滚轮缩放 · 按住拖动 · 双击重置 · Esc 关闭

实际运行源码

<script setup lang="ts">
import { computed, ref } from "vue";
type SortOrder = "asc" | "desc";
const categories = ["机械键盘", "无线鼠标", "显示器", "前端书籍"];
const products = Array.from({ length: 5_000 }, (_, index) => ({
id: index + 1,
name: `${categories[index % categories.length]} ${String(index + 1).padStart(4, "0")}`,
price: 99 + ((index * 37) % 900),
}));
const priceFormatter = new Intl.NumberFormat("zh-CN", {
style: "currency",
currency: "CNY",
maximumFractionDigits: 0,
});
const keyword = ref("");
const sortOrder = ref<SortOrder>("asc");
const cartCount = ref(0);
const result = computed(() => {
const visibleProducts = products
.filter((product) => product.name.includes(keyword.value.trim()))
.sort((a, b) =>
sortOrder.value === "asc" ? a.price - b.price : b.price - a.price,
);
return {
count: visibleProducts.length,
total: visibleProducts.reduce((sum, product) => sum + product.price, 0),
preview: visibleProducts.slice(0, 5),
};
});
</script>
<template>
<div class="demo-stack">
<div class="demo-form">
<label class="demo-field">
搜索 5,000 条商品
<input
v-model="keyword"
class="demo-input"
placeholder="例如 机械键盘"
/>
</label>
<label class="demo-field">
价格排序
<select v-model="sortOrder" class="demo-select">
<option value="asc">从低到高</option>
<option value="desc">从高到低</option>
</select>
</label>
</div>
<div class="demo-card">
<span class="demo-tag">缓存的计算结果</span>
<h2>{{ result.count.toLocaleString() }} 件商品</h2>
<p>合计 {{ priceFormatter.format(result.total) }}</p>
</div>
<ul class="demo-list">
<li v-for="product in result.preview" :key="product.id">
<span>{{ product.name }}</span>
<span>{{ priceFormatter.format(product.price) }}</span>
</li>
</ul>
<div class="demo-actions">
<button class="demo-button" type="button" @click="cartCount++">
购物车 +1(当前 {{ cartCount }})
</button>
</div>
<p class="demo-muted">购物车不是依赖,更新它会复用上一次计算结果。</p>
</div>
</template>
React Vue
useMemo(() => value, [deps]) computed(() => value)
手动维护依赖数组 自动追踪回调中读取的响应式依赖
返回普通值 返回 ComputedRef,脚本中通过 .value 读取
主要用于性能优化,不保证缓存永久存在 Vue 中表达缓存派生状态的常规方式

两者的计算函数都应该保持纯粹,不要在其中请求接口、修改状态或操作 DOM。