React × Vue

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 通过手写依赖数组决定计算还是复用。

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

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

组件关系与数据流

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.4px

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

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

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

完整 Demo 源码

以下代码直接读取在线 Demo 使用的源文件,没有省略或改写。

React 源码

ReactDemo.tsx
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 源码

VueDemo.vue
<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 对照

ReactVue 3
useMemo(() => value, deps)computed(() => value)
手动维护依赖数组自动追踪响应式依赖
返回普通值返回 ComputedRef,脚本中用 .value
主要用于性能优化派生状态的常规表达方式

**先直接计算:**小数组或简单算式通常不需要 useMemo。先按派生状态的方式直接计算,确认存在明显开销后再缓存。

v-memo 缓存模板子树,不是这里的对应 API;计算函数中也不要请求接口、修改状态或操作 DOM。