05 · React 与 Vue 核心机制对比
本篇解决什么问题: 你主职已转 Flutter,React / Vue 是老本行,但"用的时候才发现细节忘了"很常见。 本篇不做重复教程,而是用机制对比的方式,把两个框架的核心差异一次性讲清, 并标注它们与 Flutter 的映射——这正是本知识库最不可替代的价值。
📌 与
flutter/区的呼应(本区核心写法):
前端 Flutter 对应 参考 React "render 即纯函数" build()同样是纯函数Flutter 02 篇 Vue 细粒度响应式 Riverpod/Signals的精确通知Flutter 27 篇 不可变数据 + 整树重渲染 Widget 不可变 + Element复用Flutter 03 篇 组件级状态管理 StatefulWidget/ 状态管理库Flutter 08 篇 ⚠️ 易变信息:React Server Components、Vue Vapor Mode、具体版本 API 会演进, 按本库约定只讲稳定的机制模型与判断维度,版本细节以官方为准。
开篇:一张表看懂两者差异
| 维度 | React | Vue 3 |
|---|---|---|
| 模板写法 | JSX(JavaScript 即模板) | 单文件组件 SFC(<template> + <script setup>) |
| 响应式实现 | 手动触发(调用 setState/dispatch) | 自动依赖追踪(基于 Proxy) |
| 重渲染粒度 | 默认整棵子树重渲染 | 组件内细粒度,只更新用到该数据的节点 |
| 更新调度 | Fiber + 并发渲染(可中断) | 异步刷新队列(nextTick 批处理) |
| 状态原语 | useState / useReducer / useContext | ref / reactive / computed / watch |
| 副作用 | useEffect(依赖数组) | watch / watchEffect + 生命周期钩子 |
| 心智模型 | "状态变了 → 重新执行函数 → 得到新 UI" | "数据变了 → 自动知道谁依赖它 → 精确改" |
一句话结论:
React 把"何时重渲染"交给你(你调
setState),它负责重新执行整个组件函数; Vue 把"谁依赖了谁"记在内部(响应式系统),它负责只改变的那一小块。 Flutter 两者都不是——它走的是声明式 + 不可变 Widget + Element 复用的第三条路。
一、React 核心机制
1.1 组件即函数(UI = f(state))
function Counter({ initial }: { initial: number }) {
const [count, setCount] = useState(initial)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}Counter是一个纯函数:同样的props+state输入,永远得到同样的 JSX 输出。- 你不手动改 DOM,而是"返回新的 UI 描述",React 负责把差异落到真实 DOM。
- 这与 Flutter 的
build()几乎同构:build()也是"输入context+ 自身状态,输出 Widget 树"。
1.2 状态与"不可变更新"
const [list, setList] = useState<Item[]>([])
// ❌ 错误:直接改原数组,React 比对引用发现没变,不会重渲染
list.push(newItem)
setList(list)
// ✅ 正确:返回新引用
setList([...list, newItem])React 的"是否重渲染"依赖引用相等(Object.is)。 这跟 Flutter 的 Widget 不可变完全对得上:你不会去改一个旧 Widget,而是 build 出新的。
1.3 重渲染的触发与范围
setState(count + 1)
↓
该组件函数重新执行(自身)
↓
其 ↓ 所有子组件默认也重新执行(除非 memo)- 默认整棵子树重渲染,靠
React.memo/useMemo/useCallback手动剪枝。 - 这是 React 性能优化的核心痛点,也是它和 Vue 最本质的区别之一。
1.4 并发渲染与调度(Fiber)
- React 内部用 Fiber 把渲染拆成可中断的小任务,配合
useTransition/useDeferredValue让低优先级更新让路给用户输入。 - 这条能力和 Flutter 的 帧调度(16.67ms 预算、优先保证响应用户输入) 目的相同,但实现完全不同:React 是 JS 层的"可中断协调",Flutter 是引擎层每帧的"build/layout/paint"流水线。
1.5 副作用:useEffect
useEffect(() => {
const id = setInterval(tick, 1000)
return () => clearInterval(id) // 清理函数
}, []) // 依赖数组:空 = 只跑一次;漏写变量是经典 bug 来源useEffect的依赖数组是 React 的"魔法点"也是"坑点"——依赖写错会导致重复执行或闭包拿到旧值。- Flutter 没有等价物:副作用通常放在
initState/dispose,生命周期清晰、不需要依赖数组。
二、Vue 核心机制
2.1 响应式:基于 Proxy 的自动追踪
import { ref, computed, watch } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
count.value++ // 自动:谁依赖 count / double 谁更新- Vue 3 用
Proxy包裹对象,读取时收集依赖、写入时通知订阅者。 - 你不需要调任何"触发更新"的函数,框架替你记住"谁读了这个数据"。
2.2 细粒度更新(与 React 的根本差异)
count.value++
↓
响应式系统知道:<template> 里第 3 行的 {{ count }} 依赖它
↓
只更新那一个文本节点,组件函数不重新执行- 所以 Vue 一般不需要
memo之类的手动剪枝:依赖是精确的。 - 这跟 Flutter 的
ValueNotifier/Riverpod的精确ref.listen思路一致——只重建真正依赖变化的那部分。
2.3 编译期优化:模板的优势
Vue 用 <template> 而非纯 JS,换来编译期静态分析:
- 静态提升:不变的节点只创建一次,不进 patch 过程。
- Patch Flag:编译时在 VNode 上打标签,运行时只检查"这个节点需要比对哪些属性"(如
class/text)。 - React 的 JSX 本质是运行时调用,少了这道编译期信息(虽然编译器也在追赶,如 React Compiler 自动 memo)。
这条差异映射到 Flutter:Flutter 的
constWidget 也是给编译器/运行时一个"可以复用"的承诺, 和 Vue 的静态提升异曲同工——都是"声明不可变 → 跳过重建"。
2.4 副作用:watch / watchEffect
watch(count, (newVal, oldVal) => { /* 精确监听 */ })
watchEffect(() => { document.title = `count=${count.value}` }) // 自动收集依赖watchEffect和 React 的useEffect表面像,但依赖是自动收集的,不用手写依赖数组——少了一个经典 bug 来源。
三、横向对比:六个核心维度
3.1 响应式:手动 vs 自动
| React | Vue | |
|---|---|---|
| 触发方 | 你调 setState | 系统自动(Proxy setter) |
| 你需关心的 | "数据变了,记得通知" | "声明数据即可" |
| 代价 | 要懂重渲染范围、会 memo | 要懂响应式边界(解构丢失响应、reactive 数组索引限制) |
3.2 重渲染粒度
- React:组件函数级。子组件默认跟着父组件一起跑,靠
memo切断。 - Vue:组件内节点级。模板编译后精确知道依赖关系,只改文本/属性节点。
3.3 模板 vs JSX
- JSX:逻辑和 UI 在同一语言里,灵活度最高,但编译器拿不到静态结构信息,优化空间小。
- 模板:表达能力受限,但编译器能做静态提升、Patch Flag,运行时更快、更易优化。
3.4 状态管理生态
| 场景 | React | Vue |
|---|---|---|
| 官方/主流 | Redux Toolkit / Zustand / Jotai | Pinia |
| 原子化 | Recoil / Jotai | Vue 自带 ref 粒度已很细 |
| 对照 Flutter | 类似 Riverpod 的全局 Provider | 类似 Signals / ValueNotifier |
选型判断(结合你已写的 27 篇状态管理): React 生态"选择太多"反而增加决策成本;Vue 官方推荐 Pinia 收敛了选择; Flutter 侧 Riverpod 的"编译期安全 + 依赖注入"是另一种成熟度。
3.5 并发与调度
- React 有可中断渲染(Fiber + 并发特性),适合大列表里保持输入流畅。
- Vue 用异步刷新队列(
nextTick批处理同帧内的多次写入),简单且够用,但没有"让路给交互"的细粒度优先级。 - Flutter 靠引擎每帧调度 +
microtask队列,三者目标一致、机制各异。
3.6 心智负担小结
| 心智负担来源 | |
|---|---|
| React | 依赖数组、memo 边界、不可变更新、并发特性 |
| Vue | 响应式陷阱(解构、reactive 限制)、ref.value 写法 |
四、与 Flutter 的三条关键映射
这是本篇的核心价值——用你正在写的 Flutter 反过来看前端两个框架。
4.1 "UI 是函数" ↔ build()
// Flutter
Widget build(BuildContext context) {
return Text('$count'); // 纯函数:同输入 → 同输出
}// React
function Counter() {
return <span>{count}</span> // 同样纯函数
}两者都是声明式、纯函数式 UI。差别只在:React 重渲染要你调
setState, Flutter 要你调setState(() {})或ref.notifyListeners()——触发机制一样,复用机制不同。
4.2 细粒度更新 ↔ 状态管理选型
- Vue 的"自动依赖追踪"≈
Signals/Riverpod的ref.watch:只有订阅者重建。 - React 的"整树重渲染"≈ 不用状态管理库、裸
StatefulWidget调setState触发整个build。 - 所以 Flutter 里你早年选 Riverpod,本质上是在 React/Vue 的"自动 vs 手动"之争里,选了 Vue 那一侧。
4.3 不可变 ↔ Widget 不可变
- React 要求你返回新的 state 引用才能触发更新,本质是"数据不可变"。
- Flutter 的 Widget 本身不可变——每次
build都产出新 Widget,靠Element树复用、靠canUpdate(runtimeType+key)决定是否复用旧 Element。 - 两句话并起来:前端用"不可变数据"避免脏检查;Flutter 用"不可变 Widget"避免状态残留——殊途同归。
五、选型 / 迁移判断框架
你已经用 Flutter 了,这篇的意义是"需要时快速判断该用哪个前端框架",不是二选一长期持有。
| 你的情况 | 倾向 | 理由 |
|---|---|---|
| 团队本就是 Vue 栈、要快速出业务 | Vue | SFC 上手快、响应式省心、Pinia 收敛 |
| 需要复杂并发交互 / 已有 React 生态 | React | 并发渲染、海量第三方库、跨端(RN)延续 |
| 已用 Flutter 做主端,前端只是管理后台 | 看团队存量 | 哪个熟用哪个,不必为"先进"切换 |
| 想和 Flutter 心智对齐、减少状态管理心智负担 | Vue + ref | 响应式模型最接近 Signals |
迁移成本提醒:
- React → Vue:要接受"模板 +
ref.value"的写法切换,响应式陷阱是主要新坑。 - Vue → React:要补"不可变更新 +
memo+ 依赖数组"三件套,性能优化心智完全不同。 - 两者 → Flutter:最大的跨越不是语言,而是没有 DOM / 没有 CSS——UI 全靠 Widget 组合(见 02 篇核心基础)。
六、相关文档
- Flutter 02 · 核心基础(Widget/布局/前端对照)
- Flutter 03 · 渲染原理与生命周期
- Flutter 08 · 状态管理 Riverpod
- Flutter 27 · 状态管理选型与横向对比
- 前端 01 · 跨端方案横向对比(含 React Native 视角)
- 前端 03 · TypeScript 进阶与 Dart 对照
本篇为
frontend/区第 5 篇(IDA05)。后续新增主题见 plan/frontend.md。