Vue3组合式API最佳实践

前言

Vue 3引入的Composition API(组合式API)是一个重大的变革。相比Options API,Composition API提供了更好的逻辑复用、更清晰的代码组织,以及更好的TypeScript支持。

本文将深入探讨Composition API的核心概念和最佳实践,帮助你写出更优雅、更可维护的Vue3代码。


一、为什么需要Composition API

1.1 Options API的局限性

Vue 2的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
export default {
data() {
return {
user: null,
posts: [],
loading: false
}
},
computed: {
userFullName() {
return `${this.user.firstName} ${this.user.lastName}`
}
},
methods: {
async fetchUser() {
this.loading = true
this.user = await api.getUser()
this.loading = false
},
async fetchPosts() {
this.loading = true
this.posts = await api.getPosts()
this.loading = false
}
},
mounted() {
this.fetchUser()
this.fetchPosts()
}
}

问题:

  • 相关逻辑分散在不同的选项中
  • 难以提取和复用逻辑
  • 大组件会变得难以维护

1.2 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
31
<script setup>
import { ref, computed, onMounted } from 'vue'

// 用户相关逻辑
const user = ref(null)
const loading = ref(false)

const userFullName = computed(() =>
`${user.value.firstName} ${user.value.lastName}`
)

async function fetchUser() {
loading.value = true
user.value = await api.getUser()
loading.value = false
}

// 文章相关逻辑
const posts = ref([])

async function fetchPosts() {
loading.value = true
posts.value = await api.getPosts()
loading.value = false
}

onMounted(() => {
fetchUser()
fetchPosts()
})
</script>

优势:

  • 逻辑按功能组织,更清晰
  • 易于提取为可复用的函数
  • 更好的TypeScript支持

二、响应式基础

2.1 ref vs reactive

ref:适用于基本类型和单个对象

1
2
3
4
5
6
7
8
9
10
import { ref } from 'vue'

const count = ref(0)
const user = ref({ name: 'Alice', age: 25 })

console.log(count.value) // 0
count.value++

console.log(user.value.name) // 'Alice'
user.value.age = 26

reactive:适用于对象和数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { reactive } from 'vue'

const state = reactive({
count: 0,
user: {
name: 'Alice',
age: 25
}
})

console.log(state.count) // 0,无需.value
state.count++

console.log(state.user.name) // 'Alice'
state.user.age = 26

2.2 什么时候用ref,什么时候用reactive

推荐使用ref的场景:

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 基本类型
const count = ref(0)
const message = ref('Hello')
const isLoading = ref(false)

// 2. 需要整体替换的对象
const user = ref({ name: 'Alice' })
user.value = { name: 'Bob' } // ✅ 响应式保持

// 3. 从API获取的数据
const data = ref(null)
data.value = await fetchData()

推荐使用reactive的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 表单数据
const form = reactive({
username: '',
email: '',
password: ''
})

// 2. 状态集合
const state = reactive({
loading: false,
error: null,
data: []
})

// 3. 配置对象
const config = reactive({
theme: 'dark',
language: 'zh-CN',
pageSize: 20
})

2.3 toRef 和 toRefs

解构reactive对象时保持响应性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { reactive, toRefs } from 'vue'

const state = reactive({
count: 0,
message: 'Hello'
})

// ❌ 错误:失去响应性
const { count, message } = state

// ✅ 正确:保持响应性
const { count, message } = toRefs(state)

count.value++ // 响应式更新

单个属性转换:

1
2
3
4
5
6
7
8
import { reactive, toRef } from 'vue'

const state = reactive({
user: { name: 'Alice', age: 25 }
})

const userName = toRef(state.user, 'name')
userName.value = 'Bob' // state.user.name也会更新

三、计算属性与侦听器

3.1 computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

// 只读计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})

// 可写计算属性
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(value) {
[firstName.value, lastName.value] = value.split(' ')
}
})

fullNameWritable.value = 'Jane Smith'
console.log(firstName.value) // 'Jane'
console.log(lastName.value) // 'Smith'

3.2 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
import { ref, watch } from 'vue'

const count = ref(0)

// 监听ref
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})

// 监听多个源
const x = ref(0)
const y = ref(0)

watch([x, y], ([newX, newY], [oldX, oldY]) => {
console.log(`x: ${oldX} → ${newX}, y: ${oldY} → ${newY}`)
})

// 监听reactive对象
const state = reactive({ count: 0 })

watch(
() => state.count,
(newValue, oldValue) => {
console.log(`state.count changed from ${oldValue} to ${newValue}`)
}
)

// 深度监听
const obj = ref({ nested: { value: 0 } })

watch(obj, (newValue, oldValue) => {
console.log('obj changed')
}, { deep: true })

3.3 watchEffect

自动追踪依赖:

1
2
3
4
5
6
7
8
9
10
11
12
import { ref, watchEffect } from 'vue'

const count = ref(0)
const message = ref('Hello')

watchEffect(() => {
console.log(`Count: ${count.value}, Message: ${message.value}`)
})

// 自动执行一次,并在count或message变化时重新执行
count.value++ // 触发执行
message.value = 'World' // 触发执行

停止监听:

1
2
3
4
5
6
const stop = watchEffect(() => {
console.log(count.value)
})

// 停止监听
stop()

四、组合式函数(Composables)

4.1 提取可复用逻辑

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
// useCounter.js
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
const count = ref(initialValue)

const double = computed(() => count.value * 2)

function increment() {
count.value++
}

function decrement() {
count.value--
}

function reset() {
count.value = initialValue
}

return {
count,
double,
increment,
decrement,
reset
}
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { useCounter } from './composables/useCounter'

const { count, double, increment, decrement, reset } = useCounter(10)
</script>

<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ double }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>

4.2 实战:useFetch

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
// useFetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)

async function doFetch() {
loading.value = true
data.value = null
error.value = null

try {
const urlValue = toValue(url)
const response = await fetch(urlValue)
data.value = await response.json()
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}

watchEffect(() => {
doFetch()
})

return { data, error, loading }
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { ref } from 'vue'
import { useFetch } from './composables/useFetch'

const userId = ref(1)
const url = computed(() => `https://api.example.com/users/${userId.value}`)

const { data, error, loading } = useFetch(url)
</script>

<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">Error: {{ error.message }}</div>
<div v-else-if="data">
<h2>{{ data.name }}</h2>
<p>{{ data.email }}</p>
</div>
</div>
</template>

4.3 实战:useLocalStorage

1
2
3
4
5
6
7
8
9
10
11
12
13
// useLocalStorage.js
import { ref, watch } from 'vue'

export function useLocalStorage(key, defaultValue) {
const storedValue = localStorage.getItem(key)
const data = ref(storedValue ? JSON.parse(storedValue) : defaultValue)

watch(data, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })

return data
}

使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { useLocalStorage } from './composables/useLocalStorage'

const theme = useLocalStorage('theme', 'light')
const user = useLocalStorage('user', { name: '', email: '' })
</script>

<template>
<div>
<select v-model="theme">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>

<input v-model="user.name" placeholder="Name">
<input v-model="user.email" placeholder="Email">
</div>
</template>

五、生命周期钩子

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
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'

export default {
setup() {
onBeforeMount(() => {
console.log('Before Mount')
})

onMounted(() => {
console.log('Mounted')
})

onBeforeUpdate(() => {
console.log('Before Update')
})

onUpdated(() => {
console.log('Updated')
})

onBeforeUnmount(() => {
console.log('Before Unmount')
})

onUnmounted(() => {
console.log('Unmounted')
})
}
}

六、依赖注入

6.1 provide / inject

父组件提供数据:

1
2
3
4
5
6
7
8
9
10
11
<script setup>
import { ref, provide } from 'vue'

const theme = ref('dark')
const updateTheme = (newTheme) => {
theme.value = newTheme
}

provide('theme', theme)
provide('updateTheme', updateTheme)
</script>

子组件注入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
<script setup>
import { inject } from 'vue'

const theme = inject('theme')
const updateTheme = inject('updateTheme')
</script>

<template>
<div :class="`theme-${theme}`">
<button @click="updateTheme('light')">Light</button>
<button @click="updateTheme('dark')">Dark</button>
</div>
</template>

6.2 提供默认值

1
2
3
4
5
6
const theme = inject('theme', 'light') // 默认值:'light'

// 工厂函数
const theme = inject('theme', () => {
return { mode: 'light' }
})

七、