useMemo 与 computed
React 使用 useMemo 缓存昂贵计算;Vue 使用 computed 声明并缓存响应式派生值。
核心结论
useMemo 是 React 的性能优化工具,需要手动声明依赖;computed 是 Vue 表达派生状态的常规抽象,会自动追踪读取的响应式依赖。两者都要求计算函数保持纯粹。
缓存是否可复用
组件关系与数据流
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:#fffdf8,stroke:#149eca,color:#25221d,stroke-width:1.2px
classDef component fill:#e4f5fa,stroke:#149eca,color:#25221d,stroke-width:2px,font-weight:600
classDef process fill:#f4fafb,stroke:#149eca,color:#25221d,stroke-width:1.6px
classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4px组件仍会重新执行;useMemo 通过手写依赖数组决定计算还是复用。
组件关系与数据流
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:#fffdf8,stroke:#2f9d6b,color:#25221d,stroke-width:1.2px
classDef component fill:#e4f4eb,stroke:#2f9d6b,color:#25221d,stroke-width:2px,font-weight:600
classDef process fill:#f2f8f4,stroke:#2f9d6b,color:#25221d,stroke-width:1.6px
classDef resource fill:#eee9df,stroke:#817a70,color:#25221d,stroke-width:1.4pxcomputed 自动收集 keyword / sortOrder;未读取 cartCount,因此缓存不失效。
完整 Demo 源码
以下代码直接读取在线 Demo 使用的源文件,没有省略或改写。
React 源码
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 源码
<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>在线观察
示例从 5,000 条商品中筛选、排序并汇总。修改无关的购物车数量时,两边都会复用结果。
React
缓存的计算结果
5,000 件商品
合计 ¥2,738,800
- 机械键盘 0001¥99
- 机械键盘 0901¥99
- 机械键盘 1801¥99
- 机械键盘 2701¥99
- 机械键盘 3601¥99
购物车不是依赖,更新它会复用上一次计算结果。
Vue 3
缓存的计算结果
5,000 件商品
合计 ¥2,738,800
- 机械键盘 0001¥99
- 机械键盘 0901¥99
- 机械键盘 1801¥99
- 机械键盘 2701¥99
- 机械键盘 3601¥99
购物车不是依赖,更新它会复用上一次计算结果。
API 对照
| React | Vue 3 |
|---|---|
useMemo(() => value, deps) | computed(() => value) |
| 手动维护依赖数组 | 自动追踪响应式依赖 |
| 返回普通值 | 返回 ComputedRef,脚本中用 .value |
| 主要用于性能优化 | 派生状态的常规表达方式 |
**先直接计算:**小数组或简单算式通常不需要
useMemo。先按派生状态的方式直接计算,确认存在明显开销后再缓存。
v-memo 缓存模板子树,不是这里的对应 API;计算函数中也不要请求接口、修改状态或操作 DOM。