React × Vue

useCallback 与 Vue 普通函数

React 使用 useCallback 稳定回调引用;Vue setup 中声明的普通函数天然保持稳定。

核心结论

React 组件每次 render 都会创建新的函数对象,useCallback 可以按依赖复用函数引用;Vue setup 通常只运行一次,其中声明的普通函数天然稳定。稳定引用只有在下游会比较引用时才有价值。

引用比较

组件关系与数据流

StateComponent

差异焦点函数组件反复执行,需要 useCallback 配合 memo 维持 props 引用稳定。

实线表示数据向下传递,虚线表示子组件向父组件发出通知。

flowchart TB
  accTitle: React useCallback 与 memo 引用比较
  accDescr: 任意 state 更新都会重新执行 ReactDemo,useCallback 根据依赖数组复用 handleSelect 引用,memo 再浅比较 props 决定是否跳过 ProductList 渲染。
  state(["selectedProduct + cartCount"]) --> render["ReactDemo 每次 state 更新都执行"]
  render --> callback{{"useCallback([]) 复用函数引用"}}
  render --> compare{{"memo 浅比较全部 props"}}
  callback --> compare
  compare -->|"props 变化才执行"| child["ProductList (memo)"]
  child -.->|"onSelect(product)"| state
  class state state
  class render,child component
  class callback,compare process
  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

组件每次 state 更新都会执行;useCallback 稳定函数引用,memo 再比较 props。

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

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

组件关系与数据流

StateComponent

差异焦点setup 通常只执行一次,普通函数引用天然稳定,不需要对应 Hook。

实线表示数据向下传递,虚线表示子组件向父组件发出通知。

flowchart TB
  accTitle: Vue setup 函数稳定性与响应式更新
  accDescr: setup 通常只运行一次并创建稳定的 handleSelect,selectedProduct ref 单独驱动 selectedId prop,cartCount ref 不会改变 ProductList 的 props。
  setup{{"setup 只执行一次"}} --> handler[("稳定 handleSelect 函数")]
  selected(["selectedProduct ref"]) -->|"selectedId prop"| child["ProductList"]
  cart(["cartCount ref"]) --> parent["VueDemo 自身模板"]
  handler -->|"@select listener"| child
  child -.->|"emit select(product)"| selected
  class selected,cart state
  class parent,child component
  class setup process
  class handler 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

setup 创建一次普通函数;响应式依赖分别更新父模板和子组件 props。

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

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

完整 Demo 源码

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

React 源码

import { useCallback, useState } from "react";import { ProductList, type Product } from "./ProductList";export function ReactDemo() {  const [cartCount, setCartCount] = useState(0);  const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);  const handleSelect = useCallback((product: Product) => {    setSelectedProduct(product);  }, []);  return (    <div className="demo-stack">      <ProductList        selectedId={selectedProduct?.id ?? null}        onSelect={handleSelect}      />      <div className="demo-card">        <span className="demo-tag">当前选择</span>        <h2>{selectedProduct?.name ?? "尚未选择商品"}</h2>        <p>          {selectedProduct            ? `价格 ¥${selectedProduct.price}`            : "点击列表中的按钮选择商品。"}        </p>      </div>      <div className="demo-actions">        <button          className="demo-button"          type="button"          onClick={() => setCartCount((count) => count + 1)}        >          购物车 +1(当前 {cartCount})        </button>      </div>      <p className="demo-muted">        更新购物车会重新渲染父组件,但稳定的 onSelect 不会破坏 memo 缓存。      </p>    </div>  );}

Vue 3 源码

<script setup lang="ts">import { ref } from "vue";import ProductList from "./ProductList.vue";type Product = {  id: number;  name: string;  price: number;};const cartCount = ref(0);const selectedProduct = ref<Product | null>(null);function handleSelect(product: Product) {  selectedProduct.value = product;}</script><template>  <div class="demo-stack">    <ProductList      :selected-id="selectedProduct?.id ?? null"      @select="handleSelect"    />    <div class="demo-card">      <span class="demo-tag">当前选择</span>      <h2>{{ selectedProduct?.name ?? "尚未选择商品" }}</h2>      <p>        {{          selectedProduct            ? `价格 ¥${selectedProduct.price}`            : "点击列表中的按钮选择商品。"        }}      </p>    </div>    <div class="demo-actions">      <button class="demo-button" type="button" @click="cartCount++">        购物车 +1(当前 {{ cartCount }})      </button>    </div>    <p class="demo-muted">      更新购物车不会改变子组件的 props,Vue 会跳过不必要的子组件更新。    </p>  </div></template>

在线观察

增加购物车数量只更新父组件的无关状态,不应让商品列表进行不必要的工作。

React

  • 机械键盘 · ¥499
  • 无线鼠标 · ¥299
  • 显示器支架 · ¥359
当前选择

尚未选择商品

点击列表中的按钮选择商品。

更新购物车会重新渲染父组件,但稳定的 onSelect 不会破坏 memo 缓存。

Vue 3

  • 机械键盘 · ¥499
  • 无线鼠标 · ¥299
  • 显示器支架 · ¥359
当前选择

尚未选择商品

点击列表中的按钮选择商品。

更新购物车不会改变子组件的 props,Vue 会跳过不必要的子组件更新。

API 对照

ReactVue 3
useCallback(fn, deps)通常直接声明普通函数
每次 render 都重新执行组件函数setup 每个实例通常只执行一次
手动维护闭包依赖通过 ref 读取最新响应式值
常与 memo 子组件配合props 未变化时框架可跳过子组件更新

useCallback(fn, deps) 可理解为 useMemo(() => fn, deps)。只有回调传给 memo 子组件,或作为其他 Hook 的依赖时,稳定引用通常才有价值。

回调作为通信协议见组件通信;计算结果缓存见useMemo。