Vue 2 和 Vue 3 在写法哲学 上就有根本差异:Vue 2 是 Options API (按 data / methods / computed 等选项分类写),Vue 3 主推 Composition API + <script setup>(按业务逻辑聚合写),同时 Vue 3 也兼容 Options API。下面通过大量对照示例,把核心用法差异讲透。
一、创建应用实例
javascript
// Vue 2
import Vue from 'vue'
import App from './App.vue'
new Vue({
render: h => h(App)
}).$mount('#app')
javascript
// Vue 3
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 全局 API 从 Vue.xxx 改为 app.xxx
app.component('MyComp', MyComp)
app.directive('focus', focusDirective)
app.use(router)
app.mount('#app')
关键变化:
new Vue()→createApp()- 全局 API(
Vue.component、Vue.use、Vue.directive)改为app实例方法 Vue.prototype.$http = ...→app.config.globalProperties.$http = ...
二、组件写法:Options API vs Composition API
1. 计数器组件对照
Vue 2(Options API):
vue
<template>
<div>
<p>计数:{{ count }}</p>
<p>双倍:{{ double }}</p>
<button @click="increment">增加</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
export default {
data() {
return { count: 0 }
},
computed: {
double() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
},
reset() {
this.count = 0
}
},
mounted() {
console.log('组件挂载完成', this.count)
}
}
</script>
Vue 3(Composition API + setup()):
vue
<template>
<div>
<p>计数:{{ count }}</p>
<p>双倍:{{ double }}</p>
<button @click="increment">增加</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => {
count.value++
}
const reset = () => {
count.value = 0
}
onMounted(() => {
console.log('组件挂载完成', count.value)
})
return { count, double, increment, reset }
}
}
</script>
Vue 3(<script setup> 语法糖,最推荐):
vue
<template>
<div>
<p>计数:{{ count }}</p>
<p>双倍:{{ double }}</p>
<button @click="increment">增加</button>
<button @click="reset">重置</button>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => {
count.value++
}
const reset = () => {
count.value = 0
}
onMounted(() => {
console.log('组件挂载完成', count.value)
})
</script>
💡
<script setup>的优势 :无需return,顶层变量/函数/导入的组件自动暴露给模板,代码量减少 30%+,TypeScript 支持更好。
2. 响应式数据:ref vs reactive
javascript
// Vue 2:所有响应式数据都写在 data() 里
data() {
return {
count: 0,
user: { name: '张三' }
}
}
// 动态添加属性需要 this.$set
this.$set(this.user, 'age', 28)
javascript
// Vue 3:Proxy 代理,直接赋值即响应
import { ref, reactive } from 'vue'
// 基本类型用 ref,访问需 .value
const count = ref(0)
count.value++ // 修改
console.log(count.value) // 读取
// 对象/数组用 reactive
const user = reactive({ name: '张三' })
user.age = 28 // 直接生效,无需 $set
// reactive 对象直接解构会丢失响应式,需用 toRefs
const { name, age } = toRefs(user)
⚠️ 新手最容易踩的坑:
ref在 JS 中必须写.value,模板中自动解包不需要reactive不能直接解构,否则响应式丢失,要用toRefs()
3. 计算属性与侦听器
javascript
// Vue 2
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
},
watch: {
count(newVal, oldVal) {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
}
}
javascript
// Vue 3
import { computed, watch, watchEffect } from 'vue'
const fullName = computed(() => firstName.value + ' ' + lastName.value)
// 侦听单个源
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 侦听多个源
watch([count, name], ([newCount, newName]) => {
console.log(`count=${newCount}, name=${newName}`)
})
// watchEffect:自动收集依赖
watchEffect(() => {
console.log(`count 变化时触发:${count.value}`)
})
4. 生命周期钩子
| Vue 2 | Vue 3 Options API | Vue 3 Composition API |
|---|---|---|
beforeCreate |
beforeCreate |
-(setup 替代) |
created |
created |
-(setup 替代) |
beforeMount |
beforeMount |
onBeforeMount |
mounted |
mounted |
onMounted |
beforeUpdate |
beforeUpdate |
onBeforeUpdate |
updated |
updated |
onUpdated |
beforeDestroy |
beforeDestroy |
onBeforeUnmount |
destroyed |
destroyed |
onUnmounted |
javascript
// Vue 3 <script setup>
import { onMounted, onBeforeUnmount } from 'vue'
onMounted(() => {
console.log('挂载')
})
onBeforeUnmount(() => {
console.log('卸载前清理')
})
⚠️ 生命周期名称变化 :
beforeDestroy→onBeforeUnmount,destroyed→onUnmounted。Vue 3 的生命周期钩子必须从 vue 显式导入。
三、组件通信
1. Props 与 Emits
Vue 2:
vue
<!-- 子组件 -->
<script>
export default {
props: {
title: { type: String, required: true },
count: { type: Number, default: 0 }
},
methods: {
handleClick() {
this.$emit('update', 123)
}
}
}
</script>
Vue 3(<script setup>):
vue
<script setup>
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 }
})
const emit = defineEmits(['update'])
const handleClick = () => {
emit('update', 123)
}
// 使用 TS 类型声明(推荐)
// const props = defineProps<{
// title: string
// count?: number
// }>()
</script>
📌
defineProps和defineEmits是编译器宏 ,在<script setup>中自动可用,无需导入。
2. v-model 变化(破坏性变更)
html
<!-- Vue 2:v-model 本质是 :value + @input -->
<Child v-model="value" />
<!-- 等价于 -->
<Child :value="value" @input="value = $event" />
<!-- 多值同步需要用 .sync 修饰符 -->
<Child :title.sync="title" />
html
<!-- Vue 3:v-model 默认是 :modelValue + @update:modelValue -->
<Child v-model="value" />
<!-- 等价于 -->
<Child :modelValue="value" @update:modelValue="value = $event" />
<!-- 支持多个 v-model 绑定 -->
<Child v-model:title="title" v-model:content="content" />
<!-- .sync 被移除,统一用 v-model:xxx -->
子组件接收:
javascript
// Vue 3 子组件
const props = defineProps(['modelValue', 'title'])
const emit = defineEmits(['update:modelValue', 'update:title'])
3. 跨组件通信:provide / inject
javascript
// Vue 3 祖先组件
import { provide, ref } from 'vue'
const count = ref(0)
provide('count', count) // 提供响应式数据
provide('increment', () => count.value++)
javascript
// Vue 3 后代组件
import { inject } from 'vue'
const count = inject('count', 0) // 第二个参数是默认值
const increment = inject('increment')
四、模板变化
1. 多根节点(Fragment)
html
<!-- Vue 2:必须包裹单一根元素 -->
<template>
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
html
<!-- Vue 3:支持多根节点 -->
<template>
<header>...</header>
<main>...</main>
<footer>...</footer>
</template>
2. Teleport(瞬移组件)
html
<!-- Vue 3 新增,解决弹窗层级问题 -->
<template>
<button @click="show = true">打开弹窗</button>
<Teleport to="body">
<div v-if="show" class="modal">
<p>我渲染到了 body 下</p>
<button @click="show = false">关闭</button>
</div>
</Teleport>
</template>
3. 动态指令参数
html
<!-- Vue 3 支持动态指令参数 -->
<script setup>
const propName = ref('title')
const eventName = ref('click')
</script>
<template>
<h1 :[propName]="dynamicValue">动态绑定 v-bind 参数</h1>
<button @[eventName]="handler">动态绑定 v-on 参数</button>
</template>
4. 过滤器(Filters)被移除
html
<!-- Vue 2 -->
<p>{{ message | capitalize }}</p>
html
<!-- Vue 3:使用计算属性或方法替代 -->
<p>{{ capitalize(message) }}</p>
javascript
// Vue 3 <script setup>
import { computed, ref } from 'vue'
const message = ref('hello')
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1)
五、其他破坏性变更
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 全局 API | Vue.component |
app.component |
| 原型挂载 | Vue.prototype.$http |
app.config.globalProperties.$http |
| 事件总线 | Vue.prototype.$bus = new Vue() |
移除,推荐 mitt 或 Pinia |
| $children | 支持 | 移除,用 ref + defineExpose |
| $listeners | 支持 | 合并到 $attrs |
| 自定义指令钩子 | bind, inserted, componentUpdated |
beforeMount, mounted, updated |
| keyCode 修饰符 | @keyup.13 |
移除,用 @keyup.enter |
| .sync 修饰符 | 支持 | 移除,用 v-model:xxx |
六、响应式原理差异
javascript
// Vue 2:Object.defineProperty
// 问题:无法检测对象属性新增/删除,数组索引修改
data() {
return { user: { name: '张三' } }
}
// 下面这行不会触发视图更新
this.user.age = 25
// 必须用 this.$set(this.user, 'age', 25)
javascript
// Vue 3:Proxy
const user = reactive({ name: '张三' })
// 下面这行直接触发视图更新
user.age = 25
七、完整实战:TodoList 组件对照
Vue 2:
vue
<template>
<div>
<input v-model="newTodo" @keyup.enter="addTodo" />
<ul>
<li v-for="todo in todos" :key="todo.id">
<span :class="{ done: todo.completed }" @click="toggle(todo)">
{{ todo.text }}
</span>
<button @click="remove(todo)">删除</button>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
newTodo: '',
todos: [
{ id: 1, text: '学习 Vue 2', completed: true }
]
}
},
methods: {
addTodo() {
if (!this.newTodo.trim()) return
this.todos.push({
id: Date.now(),
text: this.newTodo,
completed: false
})
this.newTodo = ''
},
toggle(todo) {
todo.completed = !todo.completed
},
remove(todo) {
this.todos = this.todos.filter(t => t.id !== todo.id)
}
}
}
</script>
Vue 3(<script setup>):
vue
<template>
<div>
<input v-model="newTodo" @keyup.enter="addTodo" />
<ul>
<li v-for="todo in todos" :key="todo.id">
<span :class="{ done: todo.completed }" @click="toggle(todo)">
{{ todo.text }}
</span>
<button @click="remove(todo)">删除</button>
</li>
</ul>
</div>
</template>
<script setup>
import { ref } from 'vue'
const newTodo = ref('')
const todos = ref([
{ id: 1, text: '学习 Vue 3', completed: true }
])
const addTodo = () => {
if (!newTodo.value.trim()) return
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
const toggle = (todo) => {
todo.completed = !todo.completed
}
const remove = (todo) => {
todos.value = todos.value.filter(t => t.id !== todo.id)
}
</script>
八、迁移建议
💡 Vue 2 已于 2023 年 12 月 31 日停止维护 ,新项目直接用 Vue 3 + Vite +
<script setup>。
老项目迁移路径:
- 先升到 Vue 2.7 :支持 Composition API 和
<script setup>(兼容模式) - 使用
@vue/compat迁移构建:在 Vue 3 中兼容大部分 Vue 2 API,并给出弃用警告 - 逐组件迁移:新功能用 Composition API,旧组件逐步改写
- 替换全局 API :
Vue.component→app.component等 - 移除废弃特性 :过滤器、
$children、.sync修饰符等 - 升级构建工具:从 Vue CLI(Webpack)迁移到 Vite
关键提醒:
<script setup>中ref必须加.value(模板中不需要)reactive对象解构要用toRefs保持响应式- 生命周期钩子需要从
vue导入 v-model默认 prop 从value改为modelValue
九、核心区别速记
| 维度 | Vue 2 | Vue 3 |
|---|---|---|
| API 风格 | Options API | Composition API + <script setup> |
| 响应式 | Object.defineProperty |
Proxy |
| 入口 | new Vue() |
createApp() |
| 根节点 | 单一根节点 | 支持多根节点(Fragment) |
| v-model | :value + @input |
:modelValue + @update:modelValue |
| 过滤器 | 支持 | 移除,用 computed/method |
| TS 支持 | 需装饰器,支持较弱 | 原生 TypeScript 重写,类型友好 |
| 性能 | 基准 | 更小、更快、Tree-shaking 友好 |
| 新组件 | 无 | Teleport、Suspense |
Vue 3 不是简单的版本升级,而是开发范式的转变------从"按选项分类"到"按逻辑聚合" 。刚开始写 Vue 3 可能会不习惯 .value,但一旦适应 Composition API,你会发现复杂组件的代码组织能力有了质的飞跃。