Vue3的重大变化概览
Vue3相比Vue2有了诸多改进:
- Composition API:全新的组件逻辑组织方式
- 性能提升:更快的渲染和更小的包体积
- TypeScript支持:原生TypeScript重写,更好的类型推断
- 新特性:Teleport、Fragments、Suspense等
- Tree-shaking支持:按需引入,减少包体积
Composition API详解
为什么需要Composition API?
在Vue2中,我们使用Options API来组织组件逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| // Vue2 Options API
export default {
data() {
return {
count: 0,
loading: false,
users: []
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
},
async fetchUsers() {
this.loading = true
try {
const response = await api.getUsers()
this.users = response.data
} finally {
this.loading = false
}
}
},
mounted() {
this.fetchUsers()
}
}
|
但在复杂组件中,相关的逻辑会分散在不同的选项中,难以维护。
Composition API的优势
Vue3的Composition API让我们可以按逻辑功能组织代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| // Vue3 Composition API
import { ref, computed, onMounted } from 'vue'
import { useUsers } from '@/composables/useUsers'
export default {
setup() {
// 计数器逻辑
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => count.value++
// 用户数据逻辑
const { users, loading, fetchUsers } = useUsers()
onMounted(() => {
fetchUsers()
})
return {
count,
doubleCount,
increment,
users,
loading,
fetchUsers
}
}
}
|
核心API详解
1. ref和reactive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| import { ref, reactive, toRefs } from 'vue'
export default {
setup() {
// ref用于基本类型
const count = ref(0)
const message = ref('Hello Vue3')
// reactive用于对象类型
const state = reactive({
user: {
name: 'John',
age: 25
},
posts: []
})
// 使用toRefs解构reactive对象
const { user, posts } = toRefs(state)
const updateUser = () => {
// 修改ref需要.value
count.value++
// 直接修改reactive对象
state.user.name = 'Jane'
state.user.age++
}
return {
count,
message,
user,
posts,
updateUser
}
}
}
|
2. computed和watch
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| import { ref, computed, watch, watchEffect } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
// computed
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 可写的computed
const fullNameWritable = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value) => {
const names = value.split(' ')
firstName.value = names[0]
lastName.value = names[1] || ''
}
})
// watch单个源
watch(firstName, (newVal, oldVal) => {
console.log(`firstName changed: ${oldVal} -> ${newVal}`)
})
// watch多个源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('Name changed')
})
// watchEffect 自动追踪依赖
watchEffect(() => {
console.log(`Full name: ${fullName.value}`)
})
return {
firstName,
lastName,
fullName,
fullNameWritable
}
}
}
|
3. 生命周期钩子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
| import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
console.log('setup - 相当于 beforeCreate/created')
onBeforeMount(() => {
console.log('onBeforeMount')
})
onMounted(() => {
console.log('onMounted')
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate')
})
onUpdated(() => {
console.log('onUpdated')
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount')
})
onUnmounted(() => {
console.log('onUnmounted')
})
}
}
|
Composables - 逻辑复用的新方式
在Vue2中,我们使用mixins来复用逻辑,但mixins有命名冲突和来源不明的问题。Vue3的composables提供了更好的解决方案:
创建一个用户管理的composable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
| // composables/useUsers.js
import { ref, reactive } from 'vue'
import { userApi } from '@/api/users'
export function useUsers() {
const users = ref([])
const loading = ref(false)
const error = ref(null)
const fetchUsers = async () => {
loading.value = true
error.value = null
try {
const response = await userApi.getUsers()
users.value = response.data
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
const addUser = async (userData) => {
try {
const response = await userApi.createUser(userData)
users.value.push(response.data)
return response.data
} catch (err) {
error.value = err.message
throw err
}
}
const deleteUser = async (userId) => {
try {
await userApi.deleteUser(userId)
users.value = users.value.filter(user => user.id !== userId)
} catch (err) {
error.value = err.message
throw err
}
}
return {
users: readonly(users),
loading: readonly(loading),
error: readonly(error),
fetchUsers,
addUser,
deleteUser
}
}
|
在组件中使用composable
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| // components/UserList.vue
import { onMounted } from 'vue'
import { useUsers } from '@/composables/useUsers'
export default {
setup() {
const {
users,
loading,
error,
fetchUsers,
deleteUser
} = useUsers()
const handleDelete = async (userId) => {
if (confirm('确定要删除这个用户吗?')) {
try {
await deleteUser(userId)
} catch (err) {
alert('删除失败')
}
}
}
onMounted(() => {
fetchUsers()
})
return {
users,
loading,
error,
handleDelete
}
}
}
|
新特性实战
1. Teleport - 传送门
Teleport让我们可以将组件的一部分渲染到DOM的其他位置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| <!-- Modal.vue -->
<template>
<teleport to="body">
<div v-if="show" class="modal-overlay" @click="close">
<div class="modal-content" @click.stop>
<slot></slot>
<button @click="close">关闭</button>
</div>
</div>
</teleport>
</template>
<script>
export default {
props: ['show'],
emits: ['close'],
setup(props, { emit }) {
const close = () => emit('close')
return { close }
}
}
</script>
<style>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
max-width: 500px;
width: 90%;
}
</style>
|
2. Fragments - 多个根元素
Vue3支持组件有多个根元素:
1
2
3
4
5
6
| <template>
<!-- Vue2中这样会报错,Vue3中完全OK -->
<header>头部内容</header>
<main>主要内容</main>
<footer>底部内容</footer>
</template>
|
3. Suspense - 异步组件处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| <!-- App.vue -->
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
export default {
components: {
AsyncComponent
}
}
</script>
|
项目迁移实战
1. 渐进式迁移策略
不需要一次性重写整个项目,可以逐步迁移:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 在Vue3项目中使用Vue2风格的组件
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
// Vue3完全兼容这种写法
}
|
2. 混合使用Options API和Composition API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
| export default {
// 传统的props定义
props: ['title'],
// 使用Composition API
setup(props) {
const count = ref(0)
const increment = () => count.value++
return {
count,
increment
}
},
// 传统的computed
computed: {
displayTitle() {
return this.title.toUpperCase()
}
},
// 传统的methods
methods: {
handleClick() {
this.increment()
console.log('Clicked!')
}
}
}
|
3. 常见迁移问题及解决方案
问题1:this.$refs的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Vue2
this.$refs.myInput.focus()
// Vue3 Composition API
import { ref } from 'vue'
setup() {
const myInput = ref(null)
const focusInput = () => {
myInput.value.focus()
}
return {
myInput,
focusInput
}
}
|
问题2:全局API的变化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| // Vue2
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
Vue.use(SomePlugin)
new Vue({
render: h => h(App)
}).$mount('#app')
// Vue3
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.use(SomePlugin)
app.mount('#app')
|
问题3:事件总线的替代方案
1
2
3
4
5
6
7
8
9
10
11
| // Vue2使用事件总线
// eventBus.js
import Vue from 'vue'
export default new Vue()
// Vue3推荐使用mitt
// eventBus.js
import mitt from 'mitt'
export default mitt()
// 或者使用Pinia/Vuex进行状态管理
|
性能优化建议
1. 使用ref vs reactive的选择
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 基本类型使用ref
const count = ref(0)
const message = ref('hello')
// 复杂对象使用reactive
const state = reactive({
user: { name: 'John', age: 25 },
posts: [],
loading: false
})
// 避免reactive包装基本类型
const badExample = reactive({ count: 0 }) // 不推荐
|
2. 合理使用readonly
1
2
3
4
5
6
7
8
9
10
11
12
| import { readonly } from 'vue'
// 在composable中暴露只读状态
export function useCounter() {
const count = ref(0)
const increment = () => count.value++
return {
count: readonly(count), // 防止外部直接修改
increment
}
}
|
3. 懒加载和代码分割
1
2
3
4
5
6
7
8
9
10
11
12
| // 路由懒加载
const routes = [
{
path: '/users',
component: () => import('./views/Users.vue')
}
]
// 组件懒加载
const HeavyComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
|
与TypeScript的完美结合
Vue3对TypeScript的支持非常出色:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| // 定义Props类型
interface Props {
title: string
count?: number
}
// 使用TypeScript的setup
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
props: {
title: String,
count: {
type: Number,
default: 0
}
},
setup(props: Props) {
const internalCount = ref(props.count)
const displayTitle = computed(() =>
`${props.title} (${internalCount.value})`
)
const increment = (): void => {
internalCount.value++
}
return {
internalCount,
displayTitle,
increment
}
}
})
|
生态系统迁移
1. 路由迁移 (Vue Router 4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| // Vue2 + Vue Router 3
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [...]
})
// Vue3 + Vue Router 4
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [...]
})
|
2. 状态管理迁移 (Vuex 4 / Pinia)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Vuex 4
import { createStore } from 'vuex'
const store = createStore({
state: { count: 0 },
mutations: {
increment(state) { state.count++ }
}
})
// 或使用Pinia(推荐)
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
})
|
实际项目经验总结
优势
- 更好的性能:重写的响应式系统更高效
- 代码组织:Composition API让逻辑复用更简单
- TypeScript支持:类型推断和检查更准确
- 包体积:支持tree-shaking,按需引入
- 开发体验:更好的IDE支持和调试体验
挑战
- 学习成本:需要学习新的API和概念
- 生态系统:部分Vue2插件需要更新
- 团队迁移:需要团队统一学习和规范
迁移建议
- 新项目直接使用Vue3
- 现有项目可以渐进式迁移
- 充分利用Composition API的优势
- 建立团队编码规范