从 Vue2 到 Vue3:一个老Vue开发者的升级指南
最近和前端同事交流发现在 vue3 的项目中还是惯性的使用 vue2 的选项式写法。这篇文章将从实践角度分享我的学习心得,希望能帮助同样准备升级的你。
为什么要升级到 Vue3?
在开始之前,我们先聊聊为什么要升级到 Vue3:
- 更好的性能:Vue3 的虚拟 DOM 重写和 Tree-shaking 支持带来了更好的性能
- 组合式 API:解决了 Vue2 中组件逻辑复用的痛点
- 更好的 TypeScript 支持:Vue3 是用 TypeScript 重写的,提供了更好的类型推导
- 更小的包体积:得益于 Tree-shaking,你可以只打包用到的功能
快速上手指南
1. 创建应用的新方式
如果你习惯了 Vue2 的写法:
// Vue2 的写法
import Vue from 'vue'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
在 Vue3 中变成了:
// Vue3 的写法
import { createApp } from 'vue'
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')
个人感受:这种改变实际上更符合直觉,创建应用然后挂载插件的方式更加线性和清晰。
2. 响应式系统的变化
在 Vue2 中,我们习惯了直接在 data 中定义数据:
// Vue2
export default {
data() {
return {
count: 0,
user: {
name: '张三',
age: 25
}
}
}
}
Vue3 中需要使用 ref 或 reactive:
// Vue3
<script setup>
import { ref, reactive } from 'vue'
// 简单类型用 ref
const count = ref(0)
// 复杂对象用 reactive
const user = reactive({
name: '张三',
age: 25
})
// 注意:在模板中使用 ref 的值不需要 .value
</script>
<template>
<div>{{ count }}</div>
<div>{{ user.name }}</div>
</template>
个人最佳实践:
- 优先使用 ref,因为它可以包装任何值
- 只在确实需要对象响应式的场景使用 reactive
- 记住在 JS 中需要用 .value 访问 ref 的值
3. 组合式 API 介绍
在 Vue3 中,我们可以用 Composition API 重写:
// Vue3 的 组合式 API
<script setup>
import { ref, onMounted } from 'vue'
// 状态集中管理
const userInfo = ref({})
const loading = ref(false)
const error = ref(null)
// 业务逻辑集中管理
const fetchUserInfo = async () => {
loading.value = true
try {
userInfo.value = await api.getUserInfo()
} catch (err) {
error.value = err
} finally {
loading.value = false
}
}
// 生命周期钩子
onMounted(() => {
fetchUserInfo()
})
</script>
为什么我推荐 Composition API:
- 相关逻辑可以组织在一起,不再需要在 data、methods、computed 之间来回跳转
- 更好的代码复用能力,可以轻松提取和复用逻辑
- 更好的类型推导支持
4. 模板语法的变化
Vue3 在模板语法方面保持了大部分与 Vue2 的兼容性,但有一些值得注意的改进:
<!-- Vue2 -->
<template>
<div>
<!-- v-for 和 v-if 优先级相同,不推荐一起使用 -->
<div v-for="item in list" v-if="item.visible">
{{ item.name }}
</div>
<!-- 只能有一个根节点 -->
<div>
<header></header>
<main></main>
<footer></footer>
</div>
</div>
</template>
<!-- Vue3 -->
<template>
<!-- 可以有多个根节点 -->
<header></header>
<main>
<!-- v-for 优先级高于 v-if,更符合直觉 -->
<div v-for="item in list" :key="item.id">
<div v-if="item.visible">{{ item.name }}</div>
</div>
</main>
<footer></footer>
</template>
主要改进:
- 支持多根节点模板
- v-for 和 v-if 优先级更明确
- 更好的 TypeScript 支持
- 更好的性能优化
5. 生命周期的变化
Vue2 的生命周期钩子都有对应的 Vue3 版本,只是需要手动导入:
// Vue3
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
// 使用方式
onMounted(() => {
console.log('组件已挂载')
})
注意:
- beforeCreate 和 created 钩子在 setup 中是不需要的,因为 setup 本身就是在这两个钩子之间执行
- destroyed 改名为 unmounted
- beforeDestroy 改名为 beforeUnmount
6. 计算属性的使用
计算属性在 Vue3 中的写法更加简洁:
// Vue2
export default {
data() {
return {
firstName: '张',
lastName: '三'
}
},
computed: {
// 只读计算属性
fullName() {
return this.firstName + this.lastName
},
// 可写计算属性
fullNameWithSetter: {
get() {
return this.firstName + this.lastName
},
set(newValue) {
[this.firstName, this.lastName] = newValue.split(' ')
}
}
}
}
// Vue3
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 只读计算属性
const fullName = computed(() => firstName.value + lastName.value)
// 可写计算属性
const fullNameWithSetter = computed({
get: () => firstName.value + lastName.value,
set: (newValue) => {
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>
7. 监听器的改进
Vue3 的 watch 和 watchEffect 提供了更强大的监听能力:
// Vue2
export default {
data() {
return {
name: '',
userInfo: {
age: 25
}
}
},
watch: {
// 监听简单属性
name(newVal, oldVal) {
console.log('name changed:', newVal, oldVal)
},
// 监听对象属性
'userInfo.age': {
handler(newVal, oldVal) {
console.log('age changed:', newVal, oldVal)
},
deep: true,
immediate: true
}
}
}
// Vue3
<script setup>
import { ref, reactive, watch, watchEffect } from 'vue'
const name = ref('')
const userInfo = reactive({
age: 25
})
// 监听 ref
watch(name, (newVal, oldVal) => {
console.log('name changed:', newVal, oldVal)
})
// 监听多个来源
watch([name, () => userInfo.age], ([newName, newAge], [oldName, oldAge]) => {
console.log('values changed')
})
// 自动追踪依赖
watchEffect(() => {
console.log(`name: ${name.value}, age: ${userInfo.age}`)
})
</script>
8. Props 和 Emits 的声明
Vue3 提供了更明确的 Props 和 Emits 声明方式:
// Vue2
export default {
props: {
title: {
type: String,
required: true,
default: ''
}
},
methods: {
handleClick() {
this.$emit('update', { value: 'newValue' })
}
}
}
// Vue3
<script setup>
import { defineProps, defineEmits } from 'vue'
// Props 类型声明
const props = defineProps({
title: {
type: String,
required: true,
default: ''
}
})
// Emits 声明
const emit = defineEmits({
// 带验证的事件声明
update: (payload) => {
if (payload.value) return true
return false
}
})
const handleClick = () => {
emit('update', { value: 'newValue' })
}
</script>
<!-- 使用 TypeScript 时的声明方式 -->
<script setup lang="ts">
const props = defineProps<{
title: string
count?: number
}>()
const emit = defineEmits<{
(e: 'update', payload: { value: string }): void
(e: 'delete', id: number): void
}>()
</script>
Props 和 Emits 的主要改进:
- 更好的类型推导
- 运行时验证
- 更清晰的事件类型定义
- 可以使用 TypeScript 的类型标注
9. 插槽(Slots)的使用变化
// Vue2
<template>
<div>
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽 -->
<slot name="header"></slot>
<!-- 作用域插槽 -->
<slot name="content" :data="data"></slot>
</div>
</template>
// Vue3
<template>
<!-- 默认插槽 - 基本相同 -->
<slot></slot>
<!-- 具名插槽 - 推荐使用 v-slot 指令 -->
<slot name="header"></slot>
<!-- 作用域插槽 - 更简洁的语法 -->
<slot name="content" :data="data"></slot>
</template>
<!-- 使用方式 -->
<MyComponent>
<!-- Vue3 中统一使用 v-slot 语法,不再推荐 slot-scope -->
<template #default>默认内容</template>
<template #header>头部内容</template>
<template #content="{ data }">
{{ data }}
</template>
</MyComponent>
10. Teleport 组件
Vue3 新增了 Teleport 组件,可以将内容渲染到 DOM 的其他位置:
<!-- Vue3 新特性 -->
<template>
<div class="modal-button">
<button @click="showModal = true">打开模态框</button>
<!-- 将模态框传送到 body 下 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<div>模态框内容</div>
<button @click="showModal = false">关闭</button>
</div>
</Teleport>
</div>
</template>
11. 异步组件的改进
// Vue2
const AsyncComp = () => ({
component: import('./AsyncComp.vue'),
loading: LoadingComponent,
error: ErrorComponent,
delay: 200,
timeout: 3000
})
// Vue3
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent({
loader: () => import('./AsyncComp.vue'),
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
delay: 200,
timeout: 3000
})
// 简写方式
const AsyncComp = defineAsyncComponent(() => import('./AsyncComp.vue'))
12. 全局 API 的改变
// Vue2
import Vue from 'vue'
Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
// Vue3
import { createApp } from 'vue'
const app = createApp(App)
app.config.compilerOptions.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
13. 自定义指令的变化
// Vue2
Vue.directive('highlight', {
bind(el, binding) {},
inserted(el, binding) {},
update(el, binding) {},
componentUpdated(el, binding) {},
unbind(el, binding) {}
})
// Vue3
app.directive('highlight', {
beforeMount(el, binding) {}, // 替代 bind
mounted(el, binding) {}, // 替代 inserted
beforeUpdate(el, binding) {}, // 新增
updated(el, binding) {}, // 替代 update + componentUpdated
beforeUnmount(el, binding) {}, // 新增
unmounted(el, binding) {} // 替代 unbind
})
14. 过渡动画的变化
<!-- Vue2 -->
<transition name="fade">
<p v-if="show">hello</p>
</transition>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
<!-- Vue3 -->
<Transition name="fade">
<p v-if="show">hello</p>
</Transition>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity .5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
主要变化:
- 过渡类名改变:
v-enter改为v-enter-from - 组件名称大写:
transition改为Transition - 新增
persisted钩子用于处理缓存组件的过渡
性能优化的改进
Vue3 在性能方面有显著提升:
-
更好的 Tree-shaking 支持
- 按需引入 API
- 更小的打包体积
-
Fragment 支持
- 不再需要根节点包裹
- 减少 DOM 层级
-
静态提升
- 静态节点被提升到渲染函数之外
- 减少重复创建开销
-
Proxy 响应式系统
- 更好的性能
- 更完善的响应式支持
升级建议
-
渐进式迁移
- 新功能用 Vue3 + Composition API
- 老项目可以继续使用 Options API,Vue3 完全兼容
- 随着对 Composition API 的熟悉,逐步重构老代码
-
使用
<script setup>- 这是 Vue3 的推荐写法
- 减少了模板引用的复杂度
- 提供了更好的开发体验
-
TypeScript 支持
- 即使不用 TypeScript,也建议看看类型定义
- 配合 VS Code,可以获得很好的类型提示
-
响应式系统使用建议
- 优先使用 ref
- 必要时使用 reactive
- 注意 ref 的 .value 使用
结语
作为一个经历过从 Vue2 到 Vue3 迁移的开发者,我认为这次升级绝对值得。虽然学习曲线有点陡,但 Composition API 带来的好处远超过学习成本。建议大家在新项目中直接使用 Vue3,享受新版本带来的各种改进。
记住:Vue3 不是一个完全不同的框架,它是 Vue2 的自然进化,保持耐心,你会发现新的开发方式更加优雅和高效。