Vue vs React 核心差异全对比:一份从设计哲学到指令、事件、插槽的实战速查表
前言:差异不在语法,在哲学
很多人第一次从 Vue 迁到 React,会经历这样的困惑:
js
emit('add', item) // Vue:子组件发了个事件
props.onAdd(item) // React:子组件调了个函数
看起来只是写法不同,但背后是两套完全不同的心智模型。本文按 「根因 → 高频场景 → 迁移清单 → Vue 指令全景」 的顺序整理,既可以当科普读,也可以当迁移时的速查手册。
一、先看根因:两种设计哲学
所有表面差异,都能追溯到下面这张表:
| 维度 | Vue | React |
|---|---|---|
| 编程范式 | 模板 DSL + 指令(编译期优化) | JSX = 纯 JS 表达式(运行时协调) |
| DOM 操作 | 指令/模板声明,框架接管 | 手写 JS + Hooks + ref |
| 心智模型 | "声明做什么" | "用 JS 表达做什么" |
| 响应式 | Proxy 自动追踪依赖 | setState 触发重渲染 + 手动依赖数组 |
一句话总结:
Vue 用指令屏蔽细节,React 用 JS 暴露一切。
理解了这句话,后面的所有对照表都只是它的注脚。
二、子传父:Vue 是"发事件",React 是"传函数"
| Vue | React |
|---|---|
子 emit('send-msg', data) |
子调用 props.onSendMsg(data) |
父 @send-msg="fn" |
父 <Child onSendMsg={fn} /> |
需 defineEmits(['send-msg']) 声明 |
无需声明,靠约定 + TS 类型 |
| 事件名 kebab-case | Prop 名 camelCase + on 前缀 |
js
// 核心区别:Vue 是"事件",React 是"传函数"
emit('add', item) // Vue
props.onAdd(item) // React
在 Vue 里,事件是一个有名字的"通道",需要显式声明;在 React 里,回调本身就是 prop,类型系统(TS)替代了事件声明。
三、事件绑定:从 @click 到 onClick
| Vue | React |
|---|---|
@click="fn" |
onClick={fn} |
@click="fn('a', $event)" |
onClick={e => fn('a', e)} |
修饰符 .stop / .prevent |
手动 e.stopPropagation() / e.preventDefault() |
.self |
if (e.target !== e.currentTarget) return |
.enter / .esc 按键修饰符 |
if (e.key === 'Enter') |
.once |
用 ref 标记 / 一次性函数 |
.native(Vue 2) |
直接绑 onClick(React 无此区分) |
从这张表能看出一个规律:Vue 用修饰符把常见 DOM 操作语法化,React 则把它交还给原生事件对象。
四、指令 ↔ JS 表达式:一张表打通
这是 Vue 开发者迁 React 时查得最频繁的一张表:
| Vue 指令 | React 等价写法 |
|---|---|
v-if="c" |
{c && <A />} 或 {c ? <A/> : <B/>} |
v-else-if / v-else |
嵌套三元 / 提前 return / 查表对象 |
v-show="c" |
style={{ display: c ? '' : 'none' }} |
v-for="i in list" |
list.map(i => <X key={i.id} />) |
v-for 遍历对象 |
Object.entries(obj).map(([k, v]) => ...) |
:prop="v" |
prop={v} |
v-bind="obj" |
{...obj} |
v-model="v" |
value={v} onChange={e => setV(e.target.value)} |
v-html="s" |
dangerouslySetInnerHTML={{ __html: s }} |
v-text |
{text} |
v-slot |
children / JSX prop / render props |
v-once / v-memo |
React.memo / useMemo / useCallback |
v-pre / v-cloak |
无对应(JSX 不做插值编译;无闪烁问题) |
:class="obj" |
className={clsx(obj)}(社区库) |
:style="obj" |
style={{ color: 'red' }}(驼峰) |
一个值得注意的点:v-pre / v-cloak 在 React 中没有对应物 ------因为 JSX 根本不存在"模板插值编译"这一阶段,自然也就没有 {{ }} 闪烁问题。这就是"编译期优化"与"运行时协调"的差异体现。
五、v-model:Vue 的语法糖,React 的手工活
| Vue | React |
|---|---|
v-model="text" |
value={text} + onChange |
.trim / .number / .lazy |
在 onChange 里手动 trim() / Number() / 换 onBlur |
组件级 v-model |
约定 value + onChange 两个 prop |
多字段 v-model:name |
一个 form state + updateField('name') |
jsx
// React 标准受控组件写法
<input value={text} onChange={e => setText(e.target.value)} />
💡 React 官方没有双向绑定语法糖 ,这是 Vue 转 React 最不适应的地方。Vue 的
v-model本质就是:value+@input的语法糖,React 只是把这层糖拆开还给你。
六、插槽:从 slot 到 children / Render Props
| Vue | React |
|---|---|
默认插槽 <slot /> |
props.children |
具名插槽 <slot name="header"> |
<Card header={<h1/>} />(JSX 当 prop 传) |
作用域插槽 #default="{ item }" |
Render Props :renderItem={item => <b>{item}</b>} |
<template #xxx> 声明插槽内容 |
直接把 JSX 作为 prop 传入 |
React 没有"插槽"这个专门概念,因为 JSX 本身就是一等公民的表达式------它能出现在任何 JS 能出现的地方,于是"插槽"就退化成了"传值"。
七、自定义指令 → 自定义 Hook
| Vue 自定义指令 | React 自定义 Hook |
|---|---|
app.directive('focus', {...}) 注册 |
Hook 直接 import 使用 |
mounted(el, binding) |
useEffect(() => {...}, []) |
updated(el, binding) |
useEffect(() => {...}, [value]) |
unmounted |
useEffect 返回的清理函数 |
binding.value |
Hook 参数 |
binding.modifiers |
配置对象 / 额外参数 |
需要 el(DOM 引用) |
useRef + ref={} |
一个典型的等价改写------自动聚焦:
js
// Vue:v-focus 指令
// React:
function AutoFocusInput(props) {
const ref = useRef(null)
useEffect(() => ref.current?.focus(), [])
return <input ref={ref} {...props} />
}
Vue 的指令是"给元素挂行为",React 的 Hook 是"在组件里跑逻辑再绑到 ref 上"------前者是声明式的 DOM 扩展 ,后者是可组合的逻辑单元。
八、生命周期对照
| 概念 | Vue (Composition API) | React Hooks |
|---|---|---|
| 挂载 | onMounted |
useEffect(() => {}, []) |
| 更新 | onUpdated |
useEffect(() => {}, [deps]) |
| 卸载 | onUnmounted |
useEffect 返回清理函数 |
| 副作用 | watchEffect / watch |
useEffect + 依赖数组 |
| 计算属性 | computed |
useMemo |
| 组件级共享状态 | provide/inject |
Context + useContext |
React 用一个 useEffect + 依赖数组覆盖了 Vue 的 mounted / updated / unmounted 三个钩子,这种"收束"是 React 的一贯风格。
九、性能优化:自动 vs 手动
| 目标 | Vue | React |
|---|---|---|
| 组件不重渲染 | 自动依赖追踪(无需管) | React.memo |
| 缓存昂贵计算 | computed |
useMemo(() => fn(), [deps]) |
| 缓存函数引用 | 无需管 | useCallback(fn, [deps]) |
| 缓存 JSX 片段 | v-memo="[deps]" |
useMemo(() => <JSX/>, [deps]) |
| 只渲染一次 | v-once |
useState / useRef 初始化 |
Vue 自动追踪依赖,优化空间小;React 需要手动管理 memo / 依赖,容易踩"引用不稳定"的坑。
十、10 条"一句话"差异总结
- 子传父 :Vue
emit事件,React 回调 prop。 - 事件 :Vue
@click,ReactonClick,驼峰、传引用。 - 修饰符 :Vue 有
.stop/.prevent等,React 全部手写。 - 双向绑定 :Vue
v-model,React 手写value + onChange。 - 条件/循环 :Vue 指令,React 三元 /
&&/.map。 - key 必填:两者规则完全一致,都不建议用索引。
- 插槽 :Vue
v-slot,React JSX 当 prop / children / render props。 - 自定义指令 :Vue
directives,React 自定义 Hook +useEffect。 - 优化 :Vue 自动依赖追踪,React 手动 memo / useMemo / useCallback。
- 更新状态 :Vue 直接赋值,React
setState+ 函数式更新prev => ...防旧值。
附录:Vue 指令全景速查
既然对比的起点是"指令",那就有必要把 Vue 指令本身讲清楚------它是 Vue 区别于 React 最鲜明的语法特征。
1. 指令的本质与结构
Vue 指令是带有 v- 前缀的特殊 HTML 属性,本质是将响应式数据与 DOM 行为连接起来的声明式语法 ,让你无需手动 querySelector 或 addEventListener,页面就能随数据自动变化。
一个完整指令可以拆解为:
arduino
v-指令名:参数.修饰符="表达式"
例如 v-on:click.stop="handleClick" 中,v-on 是指令名,click 是参数,stop 是修饰符,handleClick 是表达式。
2. 内置指令全览(以 Vue 3 为准)
📝 内容渲染
| 指令 | 作用 | 关键点 |
|---|---|---|
v-text |
更新元素的 textContent |
会覆盖元素内所有原有内容 |
v-html |
更新元素的 innerHTML |
存在 XSS 风险,切勿用在用户输入上;scoped 样式不生效 |
v-pre |
跳过该元素及其子元素的编译 | 用于显示原始 {{ }},提升静态内容编译速度 |
v-once |
只渲染一次 | 数据后续变化不再更新,用于性能优化 |
v-memo |
Vue 3.2+,缓存模板片段 | 传依赖数组,仅在依赖变化时重渲染,用于大型列表极致优化 |
🔀 条件渲染
| 指令 | 作用 | 关键点 |
|---|---|---|
v-if |
根据真假值销毁或重建元素/组件 | 切换开销大,适合初始为假或很少切换 的场景;可配合 <template> |
v-else-if / v-else |
构成 v-if 的链式分支 |
前一兄弟元素必须是 v-if 或 v-else-if |
v-show |
通过 display 切换可见性 |
元素始终在 DOM 中,切换开销小,适合频繁切换 |
🔄 列表渲染
| 指令 | 作用 | 关键点 |
|---|---|---|
v-for |
遍历数组、对象、数字或字符串渲染列表 | 必须提供唯一 :key(推荐数据 ID 而非索引),避免状态错乱 |
🔗 数据绑定
| 指令 | 作用 | 关键点 |
|---|---|---|
v-bind |
动态绑定一个或多个属性/Prop | 简写 :。可绑 class、style、href,支持对象语法 |
v-model |
表单/组件上的双向绑定 | 本质是 :value + @input 的语法糖;Vue 3 支持多个 v-model 和自定义修饰符 |
⚡ 事件监听
| 指令 | 作用 | 关键点 |
|---|---|---|
v-on |
监听 DOM 事件或组件自定义事件 | 简写 @,可配合大量事件修饰符 |
🎯 其他关键指令
| 指令 | 作用 | 关键点 |
|---|---|---|
v-slot |
声明具名插槽或作用域插槽 | 简写 #,通常用在 <template> 上 |
v-cloak |
保持在元素上直到组件实例编译结束 | 配合 [v-cloak] { display: none } 解决插值闪烁 |
3. 修饰符
修饰符是以 . 指明的特殊后缀,用于指出指令应以特殊方式绑定。
v-on(@)事件修饰符
| 修饰符 | 作用 |
|---|---|
.stop |
阻止事件冒泡(event.stopPropagation()) |
.prevent |
阻止默认行为(event.preventDefault()) |
.self |
仅当事件由元素自身触发时才执行 |
.capture |
使用事件捕获模式 |
.once |
事件只触发一次 |
.passive |
以 { passive: true } 添加监听器,提升滚动性能 |
按键与系统修饰符
| 类别 | 修饰符 |
|---|---|
| 按键别名 | .enter, .tab, .delete(捕获"删除"和"退格"), .esc, .space, .up, .down, .left, .right |
| 系统按键 | .ctrl, .alt, .shift, .meta(Mac 上为 Command) |
| 精确匹配 | .exact(仅当只有被按下的系统按键组合时触发) |
v-bind(:)与 v-model 的修饰符
| 修饰符 | 所属指令 | 作用 |
|---|---|---|
.prop |
v-bind |
强制作为 DOM property 而非 attribute |
.camel |
v-bind |
将 kebab-case 属性名转 camelCase(常用于 SVG) |
.sync |
v-bind |
Vue 2 语法 ,Vue 3 已被 v-model:propName 替代 |
.lazy |
v-model |
同步时机从 input 改为 change |
.number |
v-model |
自动转换为数字类型 |
.trim |
v-model |
自动去除首尾空白 |
⚠️ Vue 2 特有 :
.native修饰符用于监听组件根元素的原生事件,Vue 3 已移除。
4. 自定义指令
当需要复用的逻辑涉及底层 DOM 操作(自动聚焦、懒加载、集成第三方 DOM 库)时,用自定义指令。
注册方式
- 全局 :
app.directive('focus', { ... }) - 局部 :组件
directives选项中注册 <script setup>:任何以v开头的驼峰变量都可直接用(const vFocus = {...}对应v-focus)
钩子函数(生命周期)
| 钩子(Vue 3) | 对应 Vue 2 钩子 | 触发时机与用途 |
|---|---|---|
created |
(无) | 元素属性/事件监听器应用前调用(Vue 3 新增) |
beforeMount |
bind |
元素插入 DOM 前,适合 CSS 相关操作 |
mounted |
inserted |
最常用,元素已挂载,可安全操作 DOM |
beforeUpdate |
(无直接对应) | 元素更新前调用 |
updated |
componentUpdated |
元素及子节点更新后调用 |
beforeUnmount |
(无直接对应) | 元素卸载前调用(Vue 3 新增),适合清理 |
unmounted |
unbind |
元素卸载后调用,必须在此清理资源 |
钩子参数 :每个钩子会收到 el(DOM 元素)、binding(含 value、oldValue、arg、modifiers)、vnode 等参数。
写在最后
如果把这份对照表压缩成一句话:
Vue 把常见模式编译成指令,React 把一切还原成 JavaScript。
所以迁 React 时,你不是在"换 API",而是在把指令的语义手动展开成 JS 表达式 ;反过来迁 Vue 时,你是在把 JS 逻辑收敛成声明式语法。
内置指令覆盖了绝大多数日常场景,修饰符是对指令行为的微调,而自定义指令则是遇到纯 DOM 操作复用时的"逃生舱"------它处理的正是 v-bind 这类声明式语法无法优雅解决的问题。