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) count.value++
console.log(user.value.name) 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) state.count++
console.log(state.user.name) state.user.age = 26
|
2.2 什么时候用ref,什么时候用reactive
推荐使用ref的场景:
1 2 3 4 5 6 7 8 9 10 11 12
| const count = ref(0) const message = ref('Hello') const isLoading = ref(false)
const user = ref({ name: 'Alice' }) user.value = { name: 'Bob' }
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
| const form = reactive({ username: '', email: '', password: '' })
const state = reactive({ loading: false, error: null, data: [] })
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'
|
三、计算属性与侦听器
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) console.log(lastName.value)
|
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)
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}`) })
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.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
| 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
| 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
| 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')
const theme = inject('theme', () => { return { mode: 'light' } })
|
七、