Vue 3 响应式系统 vs React 状态更新模型
对比 Vue 3 的属性级依赖追踪与 React 的状态快照、setter 和重新渲染模型。
核心结论
两边都能做到“状态变化,界面更新”,但保存状态和发现更新的方式不同:Vue 保留稳定的响应式引用并追踪属性读取;React 按 Hook 顺序找回 state,通过 setter 调度下一次 render。
React 算不算响应式?
广义上,React UI 会响应状态变化;但在原理对比中,更准确的说法是:Vue 使用响应式依赖追踪,React 使用状态驱动的声明式更新模型。
两条更新链
一张图连起完整链路
状态如何保存、被找到,并驱动下一次 UI
两边都会保存状态并重新计算 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 槽位出发。使用右上角的放大按钮可进入放大视图。
完整 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
count:0
无关状态 theme:light
- 01 / 最近事件
首次 render,读取初始 state - 02 / 组件函数已执行 1 次
- 03 / Hook 顺序
#1 count → #2 theme → #3 lastAction → #4 renderRef - 04 / 观察
更新 count 或 theme,整个 ReactDemo 都会再次执行。
Vue 3
count:0
无关状态 theme:light
- 01 / 最近赋值
创建 ref,并首次运行 watchEffect - 02 / count effect读取 count=0;已执行 1 次
- 03 / setup
仍为 1 次;组件实例与闭包继续持有同一组 refs。
一张表记住差异
| 观察维度 | Vue 3 | React |
|---|---|---|
| 核心抽象 | 稳定响应式引用与 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复习组件执行边界。