横向对比 / 已完成

React vs Vue 真实开发任务对比

按列表、搜索、弹窗、请求、路由权限、状态管理和工程化逐项横向对比。

返回文章积累

一句话:真正有用的 React vs Vue 对比,不是只说“哪个更好”,而是把同一个用户管理后台里的每个语法、每个开发要点都摆在一起看。

第 9 篇 / 共 12 篇。

本篇学完你会:按“语法 -> 组件 -> 状态 -> 请求 -> 路由 -> 权限 -> 工程化”的顺序,系统对比 React 19 和 Vue 3.5 在真实业务里的写法差异。

1. 对比规则

我们只用一个案例:用户管理后台

功能包含:

用户列表
搜索筛选
新增/编辑弹窗
删除确认
表单校验
API 请求
Loading / Error 状态
路由跳转
全局状态
权限控制
工程化目录组织

比较标准不是“谁语法少”,而是:

代码是否清楚
状态是否好追踪
业务是否好拆分
多人协作是否稳定
TypeScript 是否能帮忙
后续扩展是否方便

React Vue 真实开发任务对比图

2. 一张总表先看全局

要点React 19Vue 3.5
核心心智用 state 重新计算 UI模板绑定响应式数据
页面写法TSX / JSX.vue 单文件组件
组件声明函数组件script setup + template
文本插值{value}{{ value }}
属性绑定prop={value}:prop="value"
事件绑定onClick={fn}@click="fn"
条件渲染三元 / &&v-if / v-else
列表渲染array.map()v-for
表单双绑value + onChangev-model
本地状态useStateref / reactive
派生数据useMemocomputed
副作用useEffectonMounted / watch
DOM 引用useReftemplate ref
逻辑复用custom Hookcomposable
父传子propsprops
子传父callback propemit
内容插槽childrenslot
全局状态Context / Zustand / ReduxPinia
路由React RouterVue Router
权限组件包裹 / loader / guardroute meta + guard
样式CSS Modules / CSS-in-JS / Tailwindscoped CSS / CSS Modules / Tailwind

这张表先建立地图,下面逐项展开。

3. 页面入口和组件声明

React 里,一个页面通常就是一个函数:

export function UserManagementPage() {
  return <section>用户管理</section>;
}

Vue 里,一个页面通常是 .vue 单文件组件:

<script setup lang="ts">
const title = '用户管理';
</script>

<template>
  <section>{{ title }}</section>
</template>
对比点ReactVue
文件.tsx.vue
结构JS 函数返回 JSXscript、template、style 分区
优点JS/TS 能力完整页面结构接近 HTML
初学难点JSX 不是 HTML要理解 SFC 和响应式

4. 模板语法:JSX vs template

React 的 JSX 是“JavaScript 里写 UI”:

const title = '用户管理';

return <h1>{title}</h1>;

Vue 的 template 是“HTML 模板里绑定数据”:

<h1>{{ title }}</h1>

核心区别:

React:语法更接近 JS 表达式。
Vue:语法更接近 HTML + 指令。

如果页面逻辑很动态,React 的 JSX 会很灵活;如果页面结构比较标准,Vue 的模板会更直观。

5. 文本、属性、class、style

文本插值

React:

<span>{user.name}</span>

Vue:

<span>{{ user.name }}</span>

属性绑定

React:

<input value={keyword} placeholder="搜索用户名" />

Vue:

<input :value="keyword" placeholder="搜索用户名" />

class 绑定

React:

<span className={user.status === 'active' ? 'badge active' : 'badge disabled'}>
  {user.status}
</span>

Vue:

<span :class="['badge', user.status === 'active' ? 'active' : 'disabled']">
  {{ user.status }}
</span>

style 绑定

React:

<div style={{ color: user.status === 'active' ? '#16a34a' : '#64748b' }} />

Vue:

<div :style="{ color: user.status === 'active' ? '#16a34a' : '#64748b' }" />
要点ReactVue
class 名classNameclass
动态 classJS 表达式:class
动态 style对象:style 对象
文本{}{{}}

6. 条件渲染

用户状态为空时显示“暂无用户”。

React:

return users.length > 0 ? <UserTable users={users} /> : <p>暂无用户</p>;

或:

{error && <p className="error">{error}</p>}

Vue:

<UserTable v-if="users.length > 0" :users="users" />
<p v-else>暂无用户</p>

或:

<p v-if="error" class="error">{{ error }}</p>
对比点ReactVue
if/else三元表达式v-if / v-else
简单显示condition && <Comp />v-if="condition"
适合JS 逻辑强的场景模板结构清楚的场景

7. 列表渲染

用户列表是最常见的对比点。

React:

<tbody>
  {users.map((user) => (
    <UserRow key={user.id} user={user} />
  ))}
</tbody>

Vue:

<tbody>
  <UserRow v-for="user in users" :key="user.id" :user="user" />
</tbody>

两边都必须有稳定 key

要点ReactVue
循环方式mapv-for
keykey={user.id}:key="user.id"
常见坑用 index 做 key用 index 做 key

8. 事件绑定

点击编辑用户。

React:

<button onClick={() => openEditModal(user)}>编辑</button>

Vue:

<button @click="openEditModal(user)">编辑</button>

事件对象:

React:

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
  setKeyword(event.target.value);
}

Vue:

<input @input="handleInput" />
function handleInput(event: Event) {
  keyword.value = (event.target as HTMLInputElement).value;
}

Vue 也常用修饰符:

<form @submit.prevent="submitForm">
  <button type="submit">保存</button>
</form>

React 里要手动写:

function submitForm(event: React.FormEvent) {
  event.preventDefault();
}

9. 表单双向输入

搜索框写法最能体现差异。

React:

const [keyword, setKeyword] = useState('');

<input value={keyword} onChange={(event) => setKeyword(event.target.value)} />;

Vue:

<script setup lang="ts">
const keyword = ref('');
</script>

<template>
  <input v-model="keyword" />
</template>

表单对象:

React:

const [form, setForm] = useState<UserForm>({
  name: '',
  email: '',
  role: 'member'
});

<input
  value={form.name}
  onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))}
/>;

Vue:

const form = reactive<UserForm>({
  name: '',
  email: '',
  role: 'member'
});
<input v-model="form.name" />
对比点ReactVue
单字段value + onChangev-model
对象字段setForm + 展开旧对象直接绑定 form.name
优点数据变化路径显式表单代码少
注意不要直接改 state注意 reactive 解构

10. Props:父传子

父组件把用户传给行组件。

React:

type UserRowProps = {
  user: User;
};

function UserRow({ user }: UserRowProps) {
  return <span>{user.name}</span>;
}

Vue:

<script setup lang="ts">
defineProps<{
  user: User;
}>();
</script>

<template>
  <span>{{ user.name }}</span>
</template>

使用:

React:

<UserRow user={user} />

Vue:

<UserRow :user="user" />

共同原则:子组件不要直接修改父组件传来的对象。

11. 子传父:回调 vs emit

用户点击编辑,子组件要通知父组件。

React 用回调 props:

type UserRowProps = {
  user: User;
  onEdit: (user: User) => void;
};

function UserRow({ user, onEdit }: UserRowProps) {
  return <button onClick={() => onEdit(user)}>编辑</button>;
}

Vue 用 emit

<script setup lang="ts">
const props = defineProps<{ user: User }>();
const emit = defineEmits<{
  edit: [user: User];
}>();
</script>

<template>
  <button @click="emit('edit', props.user)">编辑</button>
</template>

父组件接收:

React:

<UserRow user={user} onEdit={openEditModal} />

Vue:

<UserRow :user="user" @edit="openEditModal" />
对比点ReactVue
子传父方式函数 propsemit 事件
语义普通 JS 函数组件事件
优点灵活直接父子边界清楚

12. children vs slot

如果我们做一个通用弹窗。

React:

type ModalProps = {
  title: string;
  children: React.ReactNode;
};

function Modal({ title, children }: ModalProps) {
  return (
    <section className="modal">
      <h2>{title}</h2>
      {children}
    </section>
  );
}

使用:

<Modal title="编辑用户">
  <UserForm />
</Modal>

Vue:

<template>
  <section class="modal">
    <h2>{{ title }}</h2>
    <slot />
  </section>
</template>

使用:

<Modal title="编辑用户">
  <UserForm />
</Modal>

命名插槽:

Vue:

<slot name="footer" />

React 通常用 props:

<Modal title="编辑用户" footer={<button>保存</button>}>
  <UserForm />
</Modal>

13. 本地状态:useState vs ref/reactive

React:

const [open, setOpen] = useState(false);
const [editingUser, setEditingUser] = useState<User | null>(null);

Vue:

const open = ref(false);
const editingUser = ref<User | null>(null);

对象状态:

React:

setQuery((current) => ({
  ...current,
  keyword: nextKeyword
}));

Vue:

query.keyword = nextKeyword;

大白话:

React:给 React 一份新状态。
Vue:修改响应式数据,Vue 自动追踪。

14. 派生数据:useMemo vs computed

统计正常用户数量。

React:

const activeCount = useMemo(
  () => users.filter((user) => user.status === 'active').length,
  [users]
);

Vue:

const activeCount = computed(() => users.value.filter((user) => user.status === 'active').length);
对比点ReactVue
APIuseMemocomputed
依赖手动写依赖数组自动追踪响应式依赖
常见坑依赖漏写computed 里做副作用

不要把请求接口放进 useMemocomputed。它们适合“算值”,不适合“做事”。

15. 副作用:useEffect vs onMounted/watch

页面首次加载用户列表。

React:

useEffect(() => {
  loadUsers();
}, []);

Vue:

onMounted(() => {
  loadUsers();
});

搜索条件变化后重新加载。

React:

useEffect(() => {
  loadUsers(query);
}, [query]);

Vue:

watch(
  () => ({ ...query }),
  () => {
    loadUsers();
  }
);
要点ReactVue
首次加载useEffect(..., [])onMounted
监听变化effect 依赖数组watch
清理副作用effect return 函数onUnmounted 或 watch cleanup
常见坑依赖数组漏写watch 过深、触发太频繁

16. DOM 引用:useRef vs template ref

让搜索框自动聚焦。

React:

const inputRef = useRef<HTMLInputElement | null>(null);

useEffect(() => {
  inputRef.current?.focus();
}, []);

return <input ref={inputRef} />;

Vue:

<script setup lang="ts">
const inputRef = ref<HTMLInputElement | null>(null);

onMounted(() => {
  inputRef.value?.focus();
});
</script>

<template>
  <input ref="inputRef" />
</template>

React 的 useRef 还常用来保存不会触发渲染的值;Vue 的 ref 既能做 DOM 引用,也能做响应式值,区别要靠上下文判断。

17. 接口请求和 loading/error

React:

const [state, setState] = useState({
  data: [] as User[],
  loading: false,
  error: null as string | null
});

async function loadUsers() {
  setState((current) => ({ ...current, loading: true, error: null }));

  try {
    const data = await fetchUsers(query);
    setState({ data, loading: false, error: null });
  } catch {
    setState({ data: [], loading: false, error: '加载失败' });
  }
}

Vue:

const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<string | null>(null);

async function loadUsers() {
  loading.value = true;
  error.value = null;

  try {
    users.value = await fetchUsers(query);
  } catch {
    error.value = '加载失败';
  } finally {
    loading.value = false;
  }
}

共同原则:

不要只写成功状态。
真实页面至少要有 loading、error、empty、success。

18. 业务逻辑复用:custom Hook vs composable

React 抽 useUsers

export function useUsers() {
  const [query, setQuery] = useState<UserQuery>({ keyword: '', status: 'all' });
  const [state, setState] = useState<UserState>({ data: [], loading: false, error: null });

  const reload = useCallback(() => {
    return loadUsers(query);
  }, [query]);

  return { query, setQuery, state, reload };
}

Vue 抽 useUsers

export function useUsers() {
  const query = reactive<UserQuery>({ keyword: '', status: 'all' });
  const users = ref<User[]>([]);
  const loading = ref(false);
  const error = ref<string | null>(null);

  async function reload() {
    users.value = await fetchUsers(query);
  }

  return { query, users, loading, error, reload };
}
对比点React custom HookVue composable
命名通常 useXxx通常 useXxx
调用位置组件或 Hook 顶层常在 setup/composable 里
状态useState 等 Hookref/reactive
注意Hook 调用顺序不能变注意响应式返回和解构

19. 全局状态:Context/Zustand vs Pinia

当前登录用户和权限适合全局。

React Context 简版:

const AuthContext = createContext<AuthState | null>(null);

export function useAuth() {
  const value = useContext(AuthContext);
  if (!value) throw new Error('AuthProvider missing');
  return value;
}

React Zustand 简版:

export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  permissions: [],
  setUser: (user) => set({ user, permissions: user.permissions })
}));

Vue Pinia:

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as CurrentUser | null,
    permissions: [] as string[]
  }),
  actions: {
    setUser(user: CurrentUser) {
      this.user = user;
      this.permissions = user.permissions;
    }
  }
});
状态React 建议Vue 建议
当前用户Context 或 ZustandPinia
权限列表Context 或 ZustandPinia
表单草稿本地 state组件本地 ref/reactive
用户列表本地 Hook 或请求缓存composable 或请求缓存
主题语言Context/ZustandPinia

20. 路由和页面跳转

React Router:

{
  path: '/admin/users',
  element: <UserManagementPage />
}

跳转:

const navigate = useNavigate();
navigate('/admin/users');

读参数:

const { id } = useParams();

Vue Router:

{
  path: '/admin/users',
  component: () => import('./UserManagementPage.vue')
}

跳转:

const router = useRouter();
router.push('/admin/users');

读参数:

const route = useRoute();
const id = route.params.id;

21. 权限控制

按钮权限。

React:

function PermissionButton({ code, children }: { code: PermissionCode; children: React.ReactNode }) {
  const can = useAuthStore((state) => state.permissions.includes(code));
  return can ? <>{children}</> : null;
}

Vue:

<button v-if="authStore.permissions.includes('user:create')">
  新增用户
</button>

路由权限。

React:

<RequirePermission code="user:read">
  <UserManagementPage />
</RequirePermission>

Vue:

{
  path: '/admin/users',
  component: UserManagementPage,
  meta: { permission: 'user:read' }
}
router.beforeEach((to) => {
  const authStore = useAuthStore();
  const permission = to.meta.permission as PermissionCode | undefined;

  if (permission && !authStore.permissions.includes(permission)) {
    return '/403';
  }
});

共同底线:前端权限只是体验,后端接口必须再次校验。

22. TypeScript 写法

共享类型:

type UserStatus = 'active' | 'disabled';

type User = {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'member';
  status: UserStatus;
};

React props:

type UserTableProps = {
  users: User[];
  onEdit: (user: User) => void;
};

Vue props:

defineProps<{
  users: User[];
}>();

Vue emits:

defineEmits<{
  edit: [user: User];
  delete: [id: number];
}>();

React event:

function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
  event.preventDefault();
}

Vue event:

function handleInput(event: Event) {
  const value = (event.target as HTMLInputElement).value;
}
对比点ReactVue
props 类型普通 TS typedefineProps<T>()
事件类型React 事件类型DOM Event 类型
组件返回JSX.Element 通常可推导SFC 模板推导
路由 meta自己封装或声明扩展 vue-router 类型

23. 样式组织

React 常见:

import styles from './user-table.module.css';

<table className={styles.table} />

Vue 常见:

<style scoped>
.table {
  width: 100%;
}
</style>

两边都可以用 Tailwind、UnoCSS、CSS Modules。

方式ReactVue
局部 CSSCSS Modulesscoped CSS
原子类Tailwind / UnoCSSTailwind / UnoCSS
动态样式JS 对象或 className:class / :style

24. 性能优化

React 常见优化:

const filteredUsers = useMemo(() => filterUsers(users, query), [users, query]);
const handleEdit = useCallback((user: User) => openEditModal(user), []);
export const UserRow = memo(function UserRow({ user }: UserRowProps) {
  return <tr>{user.name}</tr>;
});

Vue 常见优化:

const filteredUsers = computed(() => filterUsers(users.value, query));
<UserRow v-for="user in filteredUsers" :key="user.id" :user="user" />

共同优化:

问题处理
搜索太频繁debounce
表格太大分页 / 虚拟滚动
图片太大压缩 / 懒加载
首屏太慢路由懒加载
重复请求请求缓存

不要为了“看起来高级”提前堆优化。先把数据流写清楚。

25. 测试思路

React 组件测试:

render(<UserTable users={users} onEdit={vi.fn()} />);
expect(screen.getByText('小明')).toBeInTheDocument();

Vue 组件测试:

const wrapper = mount(UserTable, {
  props: { users }
});

expect(wrapper.text()).toContain('小明');

端到端测试两边类似:

await page.goto('/admin/users');
await page.getByPlaceholder('搜索用户名').fill('小明');
await expect(page.getByText('小明')).toBeVisible();

测试重点不是框架语法,而是业务结果:用户能不能搜、能不能保存、错误时有没有提示。

26. 工程化目录

React:

features/users/
  api.ts
  types.ts
  hooks/
    use-users.ts
  components/
    user-filter.tsx
    user-form.tsx
    user-table.tsx
  pages/
    user-management-page.tsx

Vue:

features/users/
  api.ts
  types.ts
  composables/
    use-users.ts
  components/
    UserFilter.vue
    UserForm.vue
    UserTable.vue
  pages/
    UserManagementPage.vue

真正到项目里,两边的工程思想很接近:

按业务模块聚合
接口层独立
类型独立
页面只负责组装
复杂逻辑抽到 hook/composable
权限和路由集中处理

27. 选型建议

情况更推荐
团队 JS/TS 能力强,喜欢函数式组合React
团队希望模板清晰,上手快Vue
后台管理系统、国内生态Vue 很常见
海外生态、复杂前端平台、跨端生态React 很常见
你要做 AI 全栈、个人产品两个都值得会

最实际的建议:

先精通一个,再能看懂另一个。
用同一个用户管理后台案例练两遍,比看十篇争论文章有用。

28. 下一步:进入生态和架构

学完这篇后,不要停在“我知道 React 和 Vue 语法差异”的层面。真实项目还需要继续理解生态和架构:

React 生态怎么选?
Vue 生态怎么选?
两边路由、状态、请求、表单、组件库、SSR、测试怎么横向比较?

最终你要带走的不是“React 胜”或“Vue 胜”,而是:

同一个业务问题,我能用清楚的数据流、组件结构和工程规范解决。

上一篇建议:大白话讲解——React vs Vue 心智模型对比.md

下一篇建议:大白话讲解——React 生态全景与项目架构.md