React × Vue

Vue 3 响应式系统 vs React 状态更新模型

对比 Vue 3 的属性级依赖追踪与 React 的状态快照、setter 和重新渲染模型。

核心结论

两边都能做到“状态变化,界面更新”,但保存状态和发现更新的方式不同:Vue 保留稳定的响应式引用并追踪属性读取;React 按 Hook 顺序找回 state,通过 setter 调度下一次 render。

React 算不算响应式?

广义上,React UI 会响应状态变化;但在原理对比中,更准确的说法是:Vue 使用响应式依赖追踪,React 使用状态驱动的声明式更新模型。

两条更新链

一张图连起完整链路

状态如何保存、被找到,并驱动下一次 UI

Vue 3React下一轮

两边都会保存状态并重新计算 UI;差别在于 Vue 用属性依赖定位 effect,React 用组件实例与 Hook 顺序定位 state 槽位。

flowchart LR
  accTitle: Vue 3 响应式与 React 状态更新双泳道
  accDescr: Vue 在 setup 中保存响应式引用,通过属性读取建立依赖,赋值后触发 effect;React 在组件实例中保存 Hook 槽位,按调用顺序读取 state,setter 入队后调度下一次 render。

  subgraph vueLane["Vue 3 · 响应式依赖"]
    direction TB
    vCreate["创建组件"] --> vSetup{{"setup 执行一次"}}
    vSetup --> vState(["ref / reactive"])
    vState --> vTrack{{"render effect 读取 / track"}}
    vTrack --> vMutate{{"事件修改属性"}}
    vMutate --> vTrigger{{"trigger / 调度 effect"}}
    vTrigger --> vPatch["重新运行更新函数 / patch UI"]
    vPatch -.->|"下一次交互"| vMutate
  end

  subgraph reactLane["React · Hook 调用顺序"]
    direction TB
    rCreate["组件实例"] --> rSlots(["Hook slots 保存 state"])
    rSlots --> rReset{{"render 前 index = 0"}}
    rReset --> rRead{{"useState 按顺序读取快照"}}
    rRead --> rSetter{{"事件调用 setter / enqueue"}}
    rSetter --> rSchedule{{"调度下一次 render"}}
    rSchedule --> rRender["重新执行组件 / commit UI"]
    rRender -.->|"下一次 render"| rReset
  end

  vCreate ~~~ rCreate

  classDef vueState fill:#fffdf8,stroke:#2f9d6b,color:#25221d,stroke-width:1.2px
  classDef vueProcess fill:#f2f8f4,stroke:#2f9d6b,color:#25221d,stroke-width:1.6px
  classDef vueComponent fill:#e4f4eb,stroke:#2f9d6b,color:#25221d,stroke-width:2px,font-weight:600
  classDef reactState fill:#fffdf8,stroke:#149eca,color:#25221d,stroke-width:1.2px
  classDef reactProcess fill:#f4fafb,stroke:#149eca,color:#25221d,stroke-width:1.6px
  classDef reactComponent fill:#e4f5fa,stroke:#149eca,color:#25221d,stroke-width:2px,font-weight:600
  class vState vueState
  class vSetup,vTrack,vMutate,vTrigger vueProcess
  class vCreate,vPatch vueComponent
  class rSlots reactState
  class rReset,rRead,rSetter,rSchedule reactProcess
  class rCreate,rRender reactComponent
  style vueLane fill:#fbfdfb,stroke:#2f9d6b,stroke-width:1px
  style reactLane fill:#fbfdfe,stroke:#149eca,stroke-width:1px

绿色泳道从 setup 与响应式属性出发;蓝色泳道从组件实例与 Hook 槽位出发。使用右上角的放大按钮可进入放大视图。

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

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

完整 Demo 源码

以下代码直接读取在线 Demo 与教学模型使用的源文件,没有省略或改写。教学模型用于解释更新机制,不是 React Fiber、Hooks 或 Vue 响应式系统的框架源码。

React 源码

import { useRef, useState } from "react";export function ReactDemo() {  const [count, setCount] = useState(0);  const [theme, setTheme] = useState<"light" | "dark">("light");  const [lastAction, setLastAction] = useState("首次 render,读取初始 state");  const renderExecutions = useRef(0);  renderExecutions.current += 1;  function incrementCount() {    setLastAction("setCount(current => current + 1)");    setCount((current) => current + 1);  }  function toggleTheme() {    setLastAction("setTheme(current => 切换主题)");    setTheme((current) => (current === "light" ? "dark" : "light"));  }  function reset() {    setLastAction("同一事件中调用 2 个 setter");    setCount(0);    setTheme("light");  }  return (    <div className="demo-stack">      <div className="demo-card">        <span className="demo-tag">当前 render snapshot</span>        <h2>count:{count}</h2>        <p>无关状态 theme:{theme}</p>      </div>      <div className="demo-actions">        <button className="demo-button" type="button" onClick={incrementCount}>          count + 1        </button>        <button className="demo-button" type="button" onClick={toggleTheme}>          切换无关状态        </button>        <button className="demo-button" type="button" onClick={reset}>          重置        </button>      </div>      <ol className="demo-trace" aria-label="React 执行观察">        <li>          <span>01 / 最近事件</span>          <code>{lastAction}</code>        </li>        <li>          <span>02 / 组件函数</span>          <strong>已执行 {renderExecutions.current} 次</strong>        </li>        <li>          <span>03 / Hook 顺序</span>          <code>#1 count → #2 theme → #3 lastAction → #4 renderRef</code>        </li>        <li>          <span>04 / 观察</span>          <p>更新 count 或 theme,整个 ReactDemo 都会再次执行。</p>        </li>      </ol>    </div>  );}

Vue 3 源码

<script setup lang="ts">import { ref, watchEffect } from "vue";const setupExecutions = 1;const count = ref(0);const theme = ref<"light" | "dark">("light");const lastAction = ref("创建 ref,并首次运行 watchEffect");const trackedEffectRuns = ref(0);const trackedSnapshot = ref("");let runCount = 0;watchEffect(() => {  const currentCount = count.value;  runCount += 1;  trackedEffectRuns.value = runCount;  trackedSnapshot.value = `读取 count=${currentCount}`;});function incrementCount() {  lastAction.value = "count.value += 1 → trigger count 依赖";  count.value += 1;}function toggleTheme() {  lastAction.value = "theme.value 切换;未触发 count 的 effect";  theme.value = theme.value === "light" ? "dark" : "light";}function reset() {  lastAction.value = "把两个 ref 恢复为初始值";  count.value = 0;  theme.value = "light";}</script><template>  <div class="demo-stack">    <div class="demo-card">      <span class="demo-tag">响应式 ref</span>      <h2>count:{{ count }}</h2>      <p>无关状态 theme:{{ theme }}</p>    </div>    <div class="demo-actions">      <button class="demo-button" type="button" @click="incrementCount">        count + 1      </button>      <button class="demo-button" type="button" @click="toggleTheme">        切换无关状态      </button>      <button class="demo-button" type="button" @click="reset">重置</button>    </div>    <ol class="demo-trace" aria-label="Vue 执行观察">      <li>        <span>01 / 最近赋值</span>        <code>{{ lastAction }}</code>      </li>      <li>        <span>02 / count effect</span>        <strong>{{ trackedSnapshot }};已执行 {{ trackedEffectRuns }} 次</strong>      </li>      <li>        <span>03 / setup</span>        <p>          仍为 {{ setupExecutions }} 次;组件实例与闭包继续持有同一组 refs。        </p>      </li>    </ol>  </div></template>

在线观察

两个 Demo 都维护 count 和无关的 theme。React 不记录组件函数读取了哪个 state;Vue 的 watchEffect 只会重新运行读取了变更属性的 effect。

React

当前 render snapshot

count:0

无关状态 theme:light

  1. 01 / 最近事件首次 render,读取初始 state
  2. 02 / 组件函数已执行 1 次
  3. 03 / Hook 顺序#1 count → #2 theme → #3 lastAction → #4 renderRef
  4. 04 / 观察

    更新 count 或 theme,整个 ReactDemo 都会再次执行。

Vue 3

响应式 ref

count:0

无关状态 theme:light

  1. 01 / 最近赋值创建 ref,并首次运行 watchEffect
  2. 02 / count effect读取 count=0;已执行 1 次
  3. 03 / setup

    仍为 1 次;组件实例与闭包继续持有同一组 refs。

一张表记住差异

观察维度Vue 3React
核心抽象稳定响应式引用与 effect 依赖图Hook 槽位、state snapshot 与 render 调度
状态保存setup 创建的引用由组件实例持有React 在组件对应的数据结构中保存 Hook state
如何感知变化拦截 ref/reactive 的读取与赋值必须调用 setter 提交更新
是否追踪属性读取是,track 记录当前 effect否,不记录组件读取了哪个 state 属性
更新工作触发订阅该属性的 effect重新执行相关函数组件并计算 UI

**一句话心智模型:**Vue 是“我记得谁读过属性,变化时通知它”;React 是“你用 setter 提交更新,我用新的 state snapshot 再执行组件”。

下一步阅读状态 API比较日常更新写法,或回到render 与 setup复习组件执行边界。