Skip to content

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 会演进, 按本库约定只讲稳定的机制模型与判断维度,版本细节以官方为准。


开篇:一张表看懂两者差异 ​

维度ReactVue 3
模板写法JSX(JavaScript 即模板)单文件组件 SFC(<template> + <script setup>)
响应式实现手动触发(调用 setState/dispatch)自动依赖追踪(基于 Proxy)
重渲染粒度默认整棵子树重渲染组件内细粒度,只更新用到该数据的节点
更新调度Fiber + 并发渲染(可中断)异步刷新队列(nextTick 批处理)
状态原语useState / useReducer / useContextref / reactive / computed / watch
副作用useEffect(依赖数组)watch / watchEffect + 生命周期钩子
心智模型"状态变了 → 重新执行函数 → 得到新 UI""数据变了 → 自动知道谁依赖它 → 精确改"

一句话结论:

React 把"何时重渲染"交给你(你调 setState),它负责重新执行整个组件函数; Vue 把"谁依赖了谁"记在内部(响应式系统),它负责只改变的那一小块。 Flutter 两者都不是——它走的是声明式 + 不可变 Widget + Element 复用的第三条路。


一、React 核心机制 ​

1.1 组件即函数(UI = f(state)) ​

tsx
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 状态与"不可变更新" ​

tsx
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 ​

tsx
useEffect(() => {
  const id = setInterval(tick, 1000)
  return () => clearInterval(id) // 清理函数
}, []) // 依赖数组:空 = 只跑一次;漏写变量是经典 bug 来源
  • useEffect 的依赖数组是 React 的"魔法点"也是"坑点"——依赖写错会导致重复执行或闭包拿到旧值。
  • Flutter 没有等价物:副作用通常放在 initState / dispose,生命周期清晰、不需要依赖数组。

二、Vue 核心机制 ​

2.1 响应式:基于 Proxy 的自动追踪 ​

ts
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 的 const Widget 也是给编译器/运行时一个"可以复用"的承诺, 和 Vue 的静态提升异曲同工——都是"声明不可变 → 跳过重建"。

2.4 副作用:watch / watchEffect ​

ts
watch(count, (newVal, oldVal) => { /* 精确监听 */ })
watchEffect(() => { document.title = `count=${count.value}` }) // 自动收集依赖
  • watchEffect 和 React 的 useEffect 表面像,但依赖是自动收集的,不用手写依赖数组——少了一个经典 bug 来源。

三、横向对比:六个核心维度 ​

3.1 响应式:手动 vs 自动 ​

ReactVue
触发方你调 setState系统自动(Proxy setter)
你需关心的"数据变了,记得通知""声明数据即可"
代价要懂重渲染范围、会 memo要懂响应式边界(解构丢失响应、reactive 数组索引限制)

3.2 重渲染粒度 ​

  • React:组件函数级。子组件默认跟着父组件一起跑,靠 memo 切断。
  • Vue:组件内节点级。模板编译后精确知道依赖关系,只改文本/属性节点。

3.3 模板 vs JSX ​

  • JSX:逻辑和 UI 在同一语言里,灵活度最高,但编译器拿不到静态结构信息,优化空间小。
  • 模板:表达能力受限,但编译器能做静态提升、Patch Flag,运行时更快、更易优化。

3.4 状态管理生态 ​

场景ReactVue
官方/主流Redux Toolkit / Zustand / JotaiPinia
原子化Recoil / JotaiVue 自带 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() ​

dart
// Flutter
Widget build(BuildContext context) {
  return Text('$count'); // 纯函数:同输入 → 同输出
}
tsx
// 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 栈、要快速出业务VueSFC 上手快、响应式省心、Pinia 收敛
需要复杂并发交互 / 已有 React 生态React并发渲染、海量第三方库、跨端(RN)延续
已用 Flutter 做主端,前端只是管理后台看团队存量哪个熟用哪个,不必为"先进"切换
想和 Flutter 心智对齐、减少状态管理心智负担Vue + ref响应式模型最接近 Signals

迁移成本提醒:

  • React → Vue:要接受"模板 + ref.value"的写法切换,响应式陷阱是主要新坑。
  • Vue → React:要补"不可变更新 + memo + 依赖数组"三件套,性能优化心智完全不同。
  • 两者 → Flutter:最大的跨越不是语言,而是没有 DOM / 没有 CSS——UI 全靠 Widget 组合(见 02 篇核心基础)。

六、相关文档 ​


本篇为 frontend/ 区第 5 篇(ID A05)。后续新增主题见 plan/frontend.md。