Vue vs React

24 分钟

为什么要对比 Vue 和 React

面试里被问到「Vue 和 React 有什么区别」的概率极高,但大部分人只能答出「一个用模板一个用 JSX」就卡住了。实际上两者在设计哲学、响应式机制、编译策略、生态选型上都有本质差异,理解这些差异不仅能应对面试,更能帮你在技术选型时做出合理判断。

本文按对比维度逐一展开,每个维度讲清楚两者的核心差异、背后原因和工程影响。

设计理念

Vue 是渐进式框架,React 是声明式 UI 库。

Vue 的设计目标是「用多少学多少」。你可以只用它做视图层渲染,也可以逐步引入路由、状态管理、SSR 等官方方案,形成完整框架。Vue 官方对路由(Vue Router)、状态管理(Pinia)、构建工具(Vite)、SSR(Nuxt)都有明确推荐,开发者的选择成本低。

React 把自己定位为「用于构建用户界面的 JavaScript 库」,只负责视图层。路由、状态管理、样式方案、数据请求全部交给社区。这意味着 React 生态更灵活,但也意味着团队需要自行做技术选型和方案整合。

面试追问:「渐进式」具体体现在哪?

Vue 的渐进式体现在架构分层上:核心库只提供响应式 + 组件系统,路由和状态管理是独立包,SSR 框架 Nuxt 又是独立项目。每一层都可选,你甚至可以通过 CDN 在传统页面里只用 Vue 做一小块交互。React 虽然也能这么用,但实践中几乎不存在「只用 React 不用任何生态包」的项目。

响应式机制

这是两者最本质的差异,也是面试最高频的追问点。

Vue:自动依赖追踪

Vue 的响应式核心是数据劫持 + 依赖收集。Vue2 用 Object.defineProperty,Vue3 用 Proxy,本质思路相同:拦截数据的读写操作,读取时收集依赖,修改时通知更新。

// Vue3 Composition API
import { ref, watchEffect } from 'vue';

const count = ref(0);

// watchEffect 自动追踪内部用到的响应式变量
watchEffect(() => {
  console.log(`count is: ${count.value}`);
});

count.value++; // 自动触发上面的 watchEffect

开发者只需要修改数据,Vue 自动知道哪些组件依赖了这份数据、需要重新渲染。这是「细粒度更新」——只有真正用到这份数据的组件才会重渲染。

React:手动触发 + 自顶向下 Diff

React 没有数据劫持,状态变更必须通过 setStateuseState 的 setter 显式触发。React 收到状态变更后,从触发更新的组件开始,自顶向下重新执行组件函数,通过 Virtual DOM Diff 找出差异再更新真实 DOM。

import { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      count is: {count}
    </button>
  );
}

React 的更新是「粗粒度」的:父组件重渲染时,所有子组件默认也会重新执行,即使子组件的 props 没有变化。这就引出了 React.memouseMemouseCallback 等优化手段的必要性。

对比总结

维度VueReact
变更检测自动(Proxy 拦截)手动(setState/setter)
更新粒度细粒度,精确到组件粗粒度,自顶向下 Diff
心智负担低,数据改了视图自动更新需关注不必要的重渲染
优化方式框架自动处理开发者手动 memo/useMemo

面试追问:Vue 的自动追踪为什么不会有性能问题?

Vue 的依赖追踪是在组件渲染时建立的,每个组件只追踪自己模板中实际用到的响应式变量。当数据变化时,只通知有依赖关系的组件更新,不存在「全树 Diff」的开销。但如果一个响应式对象非常大且嵌套很深,Proxy 递归代理也会带来初始化成本,Vue3 用了惰性代理(访问到深层属性时才代理)来缓解这个问题。

模板 vs JSX

Vue:模板语法 + 编译时优化

Vue 默认使用 HTML-like 的模板语法,模板在构建阶段会被编译为渲染函数。模板的好处是结构受限,编译器可以做静态分析和优化:

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>这段文字永远不会变</p>
    <ChildComponent :data="list" />
  </div>
</template>

Vue 编译器会分析模板,识别出哪些节点是静态的(如 <p>这段文字永远不会变</p>),在编译阶段直接提升为常量,运行时跳过它们的 Diff。这就是 Vue3 的「编译时优化」:

  • 静态提升(Static Hoisting):把不变的 VNode 提到渲染函数外部,避免每次重新创建。
  • Patch Flag:对动态节点标记类型(文本、class、style、props),Diff 时只比对标记了的部分。
  • Block Tree:将模板按动态节点分块,Diff 时直接跳过整个静态子树。

React:JSX 的灵活性

JSX 本质是 JavaScript 表达式,没有模板的结构限制,可以用任意 JS 逻辑控制渲染:

function UserList({ users, isAdmin }) {
  if (!users.length) return <Empty />;

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
          {isAdmin && <AdminBadge />}
        </li>
      ))}
    </ul>
  );
}

JSX 的灵活性是它最大的优势——条件渲染、列表渲染、组件组合全部用 JS 原生能力完成,不需要学 v-ifv-for 等指令语法。但正因为太灵活,编译器很难做静态分析优化。React 团队后来推出了 React Compiler(原 React Forget),试图在编译阶段自动插入 memoization,但这是后天补课,和 Vue 模板的天然优势不在同一起跑线上。

对比总结

维度Vue 模板React JSX
本质DSL,编译为渲染函数JavaScript 表达式
灵活性受限,但覆盖绝大多数场景完全灵活
编译优化静态提升、Patch Flag、Block Tree依赖 React Compiler(仍在推进)
学习曲线需学模板语法和指令只需会 JS
类型推断模板中类型支持较弱天然支持 TypeScript

面试追问:Vue 也支持 JSX,React 社区也有模板方案,为什么实际使用中很少跨用?

Vue 支持 JSX 但会失去大部分编译时优化,Vue 的优化策略是围绕模板设计的。React 社区的模板方案(如 Million.js 的 block)本质也是试图引入编译时优化,但 React 核心架构是围绕运行时设计的,模板方案只能优化局部场景。两者的优化路线决定了各自最佳实践的方向。

组件设计

Vue:单文件组件(SFC)

Vue 的 .vue 文件把模板、逻辑和样式放在一个文件里,天然实现了关注点的「就近组织」:

<script setup lang="ts">
import { ref } from 'vue';

const count = ref(0);
const increment = () => count.value++;
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

<style scoped>
button {
  padding: 8px 16px;
}
</style>

<script setup> 是 Vue3 推荐的组件写法,编译后自动导出组件,减少了样板代码。<style scoped> 通过编译时添加属性选择器实现样式隔离,不需要引入 CSS Modules 或 CSS-in-JS。

React:函数组件 + Hooks

React 从 16.8 开始全面转向函数组件 + Hooks:

import { useState, useCallback } from 'react';
import styles from './Counter.module.css';

function Counter() {
  const [count, setCount] = useState(0);
  const increment = useCallback(() => setCount(prev => prev + 1), []);

  return (
    <button className={styles.button} onClick={increment}>
      {count}
    </button>
  );
}

React 组件就是普通函数,逻辑复用通过自定义 Hooks 实现。样式方案需要自行选择(CSS Modules、Tailwind、styled-components 等)。

逻辑复用方式对比

方式VueReact
主流方案Composable(组合式函数)Custom Hooks
本质普通函数,内部使用响应式 API普通函数,内部使用 Hooks API
限制无调用位置限制必须在组件顶层调用,不能放在条件/循环里
响应式返回值天然是响应式的需配合 useState/useReducer
// Vue Composable
function useCounter(initial = 0) {
  const count = ref(initial);
  const increment = () => count.value++;
  return { count, increment };
}
// React Custom Hook
function useCounter(initial = 0) {
  const [count, setCount] = useState(initial);
  const increment = useCallback(() => setCount(prev => prev + 1), []);
  return { count, increment };
}

面试追问:为什么 React Hooks 有调用顺序限制,Vue Composable 没有?

React Hooks 的状态是靠调用顺序(数组索引)来关联的,所以不能在条件分支或循环中调用——否则每次渲染的调用顺序可能不一致,导致状态错乱。Vue Composable 内部使用的是独立的响应式对象(ref/reactive),每个响应式变量有自己的引用,不依赖调用顺序。这是两者响应式模型的根本差异带来的 API 设计差异。

状态管理

两个框架都有成熟的状态管理方案,但生态格局差异很大。

Vue 阵营:Pinia(官方推荐)

Vue 早期用 Vuex,Vue3 时代官方推荐 Pinia。Pinia 的设计极简,去掉了 Vuex 的 mutations 概念,直接用 actions 修改 state:

// stores/counter.ts
import { defineStore } from 'pinia';

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0);
  const doubleCount = computed(() => count.value * 2);

  function increment() {
    count.value++;
  }

  return { count, doubleCount, increment };
});

Pinia 的优势是:TypeScript 支持一等公民、不需要 mutations、支持 Composition API 风格、devtools 集成好。Vue 生态的状态管理基本收敛到 Pinia,选型成本低。

React 阵营:百花齐放

React 社区的状态管理方案非常多,主流的有:

方案特点适用场景
Redux Toolkit单一 store,严格单向数据流,中间件生态完善大型应用,需要可预测状态、时间旅行调试
Zustand极简 API,基于订阅模式,不需要 Provider中小型应用,追求轻量和简洁
Jotai原子化状态,自底向上组合需要细粒度状态管理,避免全局重渲染
Recoil原子化 + selector 派生,Facebook 出品复杂派生状态场景(目前维护不活跃)
MobX响应式方案,和 Vue 的思路接近偏好自动追踪的团队

选择多是优势也是负担——团队需要评估学习成本、包体积、性能特征和社区活跃度。面试中常被问到「你们项目用了什么状态管理,为什么选它」,需要能说清楚选型理由。

路由方案

维度Vue RouterReact Router
定位Vue 官方路由社区方案(非 React 官方)
路由守卫beforeEach / beforeEnter / beforeRouteLeaveloader / action(v6.4+)或手动在组件中处理
嵌套路由<router-view> 嵌套<Outlet> 嵌套
数据加载守卫中异步获取,或配合 Suspenseloader(v6.4+)或 useEffect / React Query
类型安全需手动扩展TanStack Router 提供类型安全路由

Vue Router 作为官方方案,和 Vue 核心的集成度更高,路由守卫覆盖了导航生命周期的各个阶段。React Router 经历了多次大版本重构(v4 → v5 → v6),API 变动较大,v6.4 引入了 loader/action 的数据路由模式,和之前的版本差异明显。近几年 TanStack Router 也在快速崛起,提供了更好的类型安全支持。

构建工具

Vue:Vite 主导

Vite 由 Vue 作者尤雨溪创建,虽然不绑定 Vue,但 Vue 生态和 Vite 的整合最为紧密。create-vue 脚手架默认使用 Vite,开发体验在当前前端工具链中属于第一梯队:

  • 开发环境:基于原生 ES Modules,按需编译,启动速度极快。
  • 生产构建:底层用 Rollup(Vite 5+ 计划切换到 Rolldown),Tree Shaking 效果好。
  • HMR:模块级热更新,修改文件后毫秒级生效。

React:多方案并存

React 生态的构建工具经历了几轮迭代:

  • Create React App(CRA):曾经的官方脚手架,基于 Webpack,2023 年后已停止维护。
  • Vite:React 社区也大量采用 Vite,create-vite 模板直接支持 React。
  • Next.js:内置 Turbopack(Webpack 继任者,Rust 实现),是当前 React 官方推荐的全栈框架。
  • Rspack:字节跳动推出的 Rust 实现的 Webpack 兼容方案,性能介于 Webpack 和 Vite 之间。

现状是 React 项目的构建工具选型更分散,没有像 Vue + Vite 这样的「官方推荐组合」。

TypeScript 支持

维度VueReact
组件类型推断<script setup lang="ts"> 支持良好函数组件天然支持 TS
Props 类型defineProps<T>() 编译时提取直接用接口/类型标注参数
模板类型检查需要 Volar 插件,IDE 依赖较重JSX 原生支持,IDE 开箱即用
泛型组件<script setup lang="ts" generic="T">普通泛型函数
生态类型覆盖Pinia、Vue Router 类型完善大部分库都有类型定义

React 在 TypeScript 支持上有先天优势——JSX 就是 JS,类型推断直接走 TypeScript 编译器。Vue 的模板需要额外的语言服务(Volar)来做类型检查,虽然 Vue3 + Volar 已经大幅改善,但在复杂场景下偶尔还会遇到类型推断不准确的情况。

面试追问:Vue 的 defineProps 是怎么做到编译时类型提取的?

defineProps<T>() 是一个编译器宏(compiler macro),不是运行时函数。Vue 编译器在构建阶段解析 TypeScript 类型定义,将其转换为运行时的 props 选项。这意味着类型信息只存在于编译阶段,不会增加运行时体积。但也因此有限制:defineProps 的泛型参数不能引用外部导入的类型(Vue 3.3+ 已部分放开此限制)。

性能优化策略

两者的性能优化哲学有根本差异:Vue 偏编译时,React 偏运行时。

Vue 的编译时优化

前面模板章节已提到,Vue 编译器通过静态提升、Patch Flag、Block Tree 在编译阶段就标记了哪些节点需要更新。运行时 Diff 只需要对比动态节点,跳过所有静态内容。

此外 Vue 的细粒度响应式保证了只有数据真正变化的组件才会重渲染,开发者几乎不需要手动做渲染优化。

React 的运行时优化

React 每次状态变更都会触发组件树的重执行,性能优化主要靠开发者手动处理:

// 1. React.memo:跳过 props 未变化的组件
const ExpensiveList = React.memo(({ items }: { items: Item[] }) => {
  return items.map(item => <ListItem key={item.id} item={item} />);
});

// 2. useMemo:缓存昂贵计算结果
const sortedItems = useMemo(
  () => items.sort((a, b) => a.name.localeCompare(b.name)),
  [items]
);

// 3. useCallback:缓存函数引用,避免子组件重渲染
const handleClick = useCallback((id: string) => {
  setSelectedId(id);
}, []);

React 的优化手段更多、更细,但也意味着更高的心智负担。React Compiler 的目标就是自动化这些优化,目前已在 Meta 内部大规模使用,但社区层面还未完全普及。

对比总结

维度VueReact
优化时机编译时运行时
默认行为自动精确更新全子树重执行
开发者负担高(需手动 memo)
上限受限于编译器能力手动优化可以做到极致
未来方向持续增强编译器React Compiler 自动 memoization

面试追问:React 为什么不一开始就做编译时优化?

React 的设计哲学是「UI 是状态的函数」(UI = f(state)),组件就是普通函数,每次渲染都是一次完整的函数调用。这种心智模型简洁统一,但也意味着框架无法在编译时预测哪些部分会变、哪些不会变——因为 JSX 中可以写任意 JS 表达式。Vue 的模板语法通过牺牲灵活性换来了可分析性,这是两条不同的技术路线,没有绝对优劣。

社区生态

维度VueReact
GitHub Stars约 210k+约 230k+
npm 周下载量约 500万+约 2500万+(含 React Native)
企业采用阿里、字节、美团、Gitlab、Apple(部分)Meta、Vercel、Airbnb、Netflix、Shopify
全栈框架NuxtNext.js、Remix
移动端无官方方案(uni-app 等社区方案)React Native
组件库Element Plus、Ant Design Vue、Naive UIAnt Design、MUI、Radix、shadcn/ui
就业市场国内需求旺盛,海外相对少全球范围需求量大

React 在全球范围的生态体量明显更大,特别是 React Native 给了它跨端能力。Vue 在国内的渗透率非常高,中后台系统大量采用 Vue + Element Plus 的技术栈。海外市场 React 占据绝对主导地位。

何时选 Vue,何时选 React

选型不是站队,是根据团队和项目情况做工程决策。以下是一些参考维度:

倾向选 Vue 的场景

  • 团队以 Vue 经验为主,切换成本高。
  • 中后台管理系统,Element Plus / Naive UI 生态成熟,开箱即用。
  • 希望减少选型决策,Vue 官方对路由、状态管理、构建工具都有明确推荐。
  • 项目对编译时性能优化有需求,不想在组件层面大量手写 memo。
  • 渐进式迁移老项目,Vue 可以嵌入传统页面,逐步替换。

倾向选 React 的场景

  • 需要跨端能力(React Native),一套技术栈覆盖 Web + App。
  • 海外业务为主,React 在海外的社区资源、招聘、开源组件都更丰富。
  • 追求最大的灵活性和可组合性,React 的函数式理念和 Hooks 模型更适合高度定制化的架构。
  • 团队 TypeScript 重度使用,React + TSX 的类型体验更原生。
  • 全栈框架需求强烈,Next.js 的 SSR/SSG/ISR 能力和 Vercel 平台深度绑定,生态领先。

不应该作为选型依据的因素

  • ❌ 「React 更难所以更高级」——复杂度不等于先进性。
  • ❌ 「Vue 性能比 React 好」——两者在绝大多数场景下性能差异可忽略。
  • ❌ 「用 React 的公司更多所以要选 React」——要看的是你的目标市场和团队现状。

总结

Vue 和 React 是当下前端两条最主流的技术路线,核心差异可以用一句话概括:Vue 通过编译器和响应式系统替开发者做了更多事,React 把控制权留给开发者用 JavaScript 解决问题。

面试中被问到这个话题,建议按以下脉络组织回答:

  1. 先说设计理念:渐进式框架 vs UI 库,决定了生态格局的差异。
  2. 再说核心机制:响应式的差异是最本质的——自动追踪 vs 手动触发,影响了更新粒度、优化策略和 API 设计。
  3. 然后说开发体验:模板 vs JSX、SFC vs 函数组件、官方生态 vs 社区生态。
  4. 最后说选型:不站队,结合团队背景、项目类型、目标市场给出理由。

记住:面试官不是要听你背诵两者的功能列表,而是要看你能否理解技术决策背后的 trade-off。每个差异背后都有设计哲学层面的原因,把这个讲清楚,比罗列十条区别更有价值。