Vue 2 和 Vue 3 在写法哲学上就有根本差异:Vue 2 是Options API(按 data / methods / computed 等选项分类写),Vue 3 主推Composition API +<script setup>(按业务逻辑聚合写),同时 Vue 3 也兼容 Options API。下面通过大量对照示例,把核心用法差异讲透。
一、创建应用实例
// Vue 2importVuefrom'vue'importAppfrom'./App.vue'newVue({render:h=>h(App)}).$mount('#app')// Vue 3import{createApp}from'vue'importAppfrom'./App.vue'constapp=createApp(App)// 全局 API 从 Vue.xxx 改为 app.xxxapp.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):
<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()):
<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>语法糖,最推荐):
<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
// Vue 2:所有响应式数据都写在 data() 里data(){return{count:0,user:{name:'张三'}}}// 动态添加属性需要 this.$setthis.$set(this.user,'age',28)// Vue 3:Proxy 代理,直接赋值即响应import{ref,reactive}from'vue'// 基本类型用 ref,访问需 .valueconstcount=ref(0)count.value++// 修改console.log(count.value)// 读取// 对象/数组用 reactiveconstuser=reactive({name:'张三'})user.age=28// 直接生效,无需 $set// reactive 对象直接解构会丢失响应式,需用 toRefsconst{name,age}=toRefs(user)⚠️新手最容易踩的坑:
ref在 JS 中必须写.value,模板中自动解包不需要reactive不能直接解构,否则响应式丢失,要用toRefs()
3. 计算属性与侦听器
// Vue 2computed:{fullName(){returnthis.firstName+' '+this.lastName}},watch:{count(newVal,oldVal){console.log(`count 从${oldVal}变为${newVal}`)}}// Vue 3import{computed,watch,watchEffect}from'vue'constfullName=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 |
// 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:
<!-- 子组件 --> <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>):
<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 变化(破坏性变更)
<!-- Vue 2:v-model 本质是 :value + @input --><Childv-model="value"/><!-- 等价于 --><Child:value="value"@input="value = $event"/><!-- 多值同步需要用 .sync 修饰符 --><Child:title.sync="title"/><!-- Vue 3:v-model 默认是 :modelValue + @update:modelValue --><Childv-model="value"/><!-- 等价于 --><Child:modelValue="value"@update:modelValue="value = $event"/><!-- 支持多个 v-model 绑定 --><Childv-model:title="title"v-model:content="content"/><!-- .sync 被移除,统一用 v-model:xxx -->子组件接收:
// Vue 3 子组件constprops=defineProps(['modelValue','title'])constemit=defineEmits(['update:modelValue','update:title'])3. 跨组件通信:provide / inject
// Vue 3 祖先组件import{provide,ref}from'vue'constcount=ref(0)provide('count',count)// 提供响应式数据provide('increment',()=>count.value++)// Vue 3 后代组件import{inject}from'vue'constcount=inject('count',0)// 第二个参数是默认值constincrement=inject('increment')四、模板变化
1. 多根节点(Fragment)
<!-- Vue 2:必须包裹单一根元素 --><template><div><header>...</header><main>...</main><footer>...</footer></div></template><!-- Vue 3:支持多根节点 --><template><header>...</header><main>...</main><footer>...</footer></template>2. Teleport(瞬移组件)
<!-- Vue 3 新增,解决弹窗层级问题 --><template><button@click="show = true">打开弹窗</button><Teleportto="body"><divv-if="show"class="modal"><p>我渲染到了 body 下</p><button@click="show = false">关闭</button></div></Teleport></template>3. 动态指令参数
<!-- Vue 3 支持动态指令参数 --><scriptsetup>constpropName=ref('title')consteventName=ref('click')</script><template><h1:[propName]="dynamicValue">动态绑定 v-bind 参数</h1><button@[eventName]="handler">动态绑定 v-on 参数</button></template>4. 过滤器(Filters)被移除
<!-- Vue 2 --><p>{{ message | capitalize }}</p><!-- Vue 3:使用计算属性或方法替代 --><p>{{ capitalize(message) }}</p>// Vue 3 <script setup>import{computed,ref}from'vue'constmessage=ref('hello')constcapitalize=(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 |
六、响应式原理差异
// Vue 2:Object.defineProperty// 问题:无法检测对象属性新增/删除,数组索引修改data(){return{user:{name:'张三'}}}// 下面这行不会触发视图更新this.user.age=25// 必须用 this.$set(this.user, 'age', 25)// Vue 3:Proxyconstuser=reactive({name:'张三'})// 下面这行直接触发视图更新user.age=25七、完整实战:TodoList 组件对照
Vue 2:
<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>):
<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,你会发现复杂组件的代码组织能力有了质的飞跃。