React × Vue

派生状态与 computed

React 在渲染期间从现有状态计算结果;Vue 使用 computed 声明缓存的派生值。

核心结论

可以从现有 props/state 推导的值,不要再存一份可变状态。React 通常在 render 中直接计算;Vue 通常用 computed 表达响应式派生关系。

单一事实来源

组件关系与数据流

StateComponentDerivedData

差异焦点没有缓存或订阅关系,派生计算跟随每次组件 render 执行。

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

flowchart TB
  accTitle: React 渲染期间直接派生
  accDescr: query 或 inStockOnly 更新后 ReactDemo 函数重新执行,products filter 在每次 render 中直接运行并生成普通 visibleProducts 值。
  filters(["query + inStockOnly state"]) --> render["ReactDemo 再次执行"]
  products[("products")] --> render
  render --> derive{{"每次 render 执行 filter"}}
  derive --> result[("普通 visibleProducts 值")]
  result --> list["商品列表 UI"]
  class filters state
  class render,list component
  class derive process
  class products,result 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

组件重新执行时直接 filter;visibleProducts 是本次 render 的普通值。

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

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

组件关系与数据流

StateComponentDerivedData

差异焦点computed 是可缓存的响应式派生值,不依赖组件整体重新执行。

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

flowchart TB
  accTitle: Vue computed 响应式派生
  accDescr: computed 首次读取时自动收集 query 与 inStockOnly refs,依赖未变时返回缓存,依赖变化后重新计算 visibleProducts 并更新列表。
  filters(["query + inStockOnly refs"]) --> tracking{{"computed 自动收集依赖"}}
  products[("products")] --> tracking
  tracking --> cache[("缓存 visibleProducts")]
  cache -->|"依赖未变直接复用"| list["商品列表 UI"]
  class filters state
  class list component
  class tracking 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 自动追踪响应式读取,并在依赖未变化时复用缓存。

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

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

完整 Demo 源码

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

React 源码

ReactDemo.tsx
import { useState } from "react";const products = [  { id: 1, name: "Apple", price: "$1", stocked: true },  { id: 2, name: "Dragonfruit", price: "$1", stocked: true },  { id: 3, name: "Passionfruit", price: "$2", stocked: false },  { id: 4, name: "Spinach", price: "$2", stocked: true },];export function ReactDemo() {  const [query, setQuery] = useState("");  const [inStockOnly, setInStockOnly] = useState(false);  const visibleProducts = products.filter((product) => {    const matchesQuery = product.name      .toLowerCase()      .includes(query.toLowerCase());    return matchesQuery && (!inStockOnly || product.stocked);  });  return (    <div className="demo-stack">      <label className="demo-field">        搜索商品        <input          className="demo-input"          value={query}          placeholder="例如 Apple"          onChange={(event) => setQuery(event.target.value)}        />      </label>      <label className="demo-field">        <span>          <input            type="checkbox"            checked={inStockOnly}            onChange={(event) => setInStockOnly(event.target.checked)}          />{" "}          仅显示有库存        </span>      </label>      <ul className="demo-list">        {visibleProducts.map((product) => (          <li key={product.id}>            <span>{product.name}</span>            <span className={product.stocked ? "" : "demo-muted"}>              {product.price} · {product.stocked ? "有库存" : "缺货"}            </span>          </li>        ))}      </ul>    </div>  );}

Vue 3 源码

VueDemo.vue
<script setup lang="ts">import { computed, ref } from "vue";const products = [  { id: 1, name: "Apple", price: "$1", stocked: true },  { id: 2, name: "Dragonfruit", price: "$1", stocked: true },  { id: 3, name: "Passionfruit", price: "$2", stocked: false },  { id: 4, name: "Spinach", price: "$2", stocked: true },];const query = ref("");const inStockOnly = ref(false);const visibleProducts = computed(() =>  products.filter((product) => {    const matchesQuery = product.name      .toLowerCase()      .includes(query.value.toLowerCase());    return matchesQuery && (!inStockOnly.value || product.stocked);  }),);</script><template>  <div class="demo-stack">    <label class="demo-field">      搜索商品      <input        v-model="query"        class="demo-input"        placeholder="例如 Apple"      />    </label>    <label class="demo-field">      <span>        <input v-model="inStockOnly" type="checkbox" />        仅显示有库存      </span>    </label>    <ul class="demo-list">      <li v-for="product in visibleProducts" :key="product.id">        <span>{{ product.name }}</span>        <span :class="{ 'demo-muted': !product.stocked }">          {{ product.price }} · {{ product.stocked ? "有库存" : "缺货" }}        </span>      </li>    </ul>  </div></template>

在线观察

输入搜索词并切换“仅显示有库存”,两个 Demo 都只保存原始商品与筛选条件。

React

  • Apple$1 · 有库存
  • Dragonfruit$1 · 有库存
  • Passionfruit$2 · 缺货
  • Spinach$2 · 有库存

Vue 3

  • Apple$1 · 有库存
  • Dragonfruit$1 · 有库存
  • Passionfruit$2 · 缺货
  • Spinach$2 · 有库存

为什么不再存一份列表

如果把 visibleProducts 也放进 state,就必须在每次来源变化时保持同步,容易产生过期数据和多余 Effect。派生值应尽量保持纯函数关系。

派生不等于缓存

React 中直接计算首先解决数据一致性;只有计算成本已经成为问题时,才进一步使用 useMemo。Vue 的 computed 同时表达派生关系并提供缓存。

昂贵计算继续阅读useMemo 与 computed;状态更新基础见useState 与 ref/reactive。