引言
Vue 3的发布带来了革命性的变化,其中最引人注目的就是Composition API的引入。作为Vue 3的核心特性之一,Composition API为开发者提供了更加灵活和强大的组件状态管理方式。相比于Vue 2的Options API,Composition API让我们能够更好地组织和复用代码逻辑,特别是在处理复杂组件时展现出巨大的优势。
本文将深入探讨Vue 3 Composition API的核心特性和使用方法,从基础概念到高级应用,通过实际项目案例展示如何构建可维护的现代化Vue应用。我们将涵盖响应式数据处理、组合函数复用、组件通信等关键知识点,帮助开发者全面掌握这一重要技术。
什么是Composition API
核心概念
Composition API是Vue 3中引入的一种新的组件逻辑组织方式。它允许我们将组件的逻辑按照功能进行分组,而不是按照选项类型进行分组。这种组织方式使得代码更加清晰、可维护,并且能够更好地复用逻辑代码。
在传统的Options API中,我们按照data、methods、computed、watch等选项来组织代码。而Composition API则允许我们将相关的逻辑组合在一起,形成更加模块化的代码结构。
与Options API的对比
让我们通过一个简单的例子来对比两种API的差异:
// Options API
export default {
data() {
return {
count: 0,
name: 'Vue'
}
},
computed: {
doubledCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
watch: {
count(newVal, oldVal) {
console.log(`count changed from ${oldVal} to ${newVal}`)
}
}
}
// Composition API
import { ref, computed, watch } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue')
const doubledCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
return {
count,
name,
doubledCount,
increment
}
}
}
响应式数据处理
ref和reactive的基础使用
Composition API的核心是响应式系统,它提供了两种主要的响应式数据处理方式:ref和reactive。
ref的使用
ref用于创建响应式的数据,适用于基本数据类型和对象:
import { ref } from 'vue'
// 基本数据类型
const count = ref(0)
const message = ref('Hello Vue')
// 对象类型
const user = ref({
name: 'John',
age: 30
})
// 访问和修改
console.log(count.value) // 0
count.value = 10
console.log(count.value) // 10
// 对象属性访问
console.log(user.value.name) // John
user.value.name = 'Jane'
reactive的使用
reactive用于创建响应式对象,适用于复杂的数据结构:
import { reactive } from 'vue'
const state = reactive({
count: 0,
user: {
name: 'John',
age: 30
},
todos: []
})
// 直接修改属性
state.count = 10
state.user.name = 'Jane'
state.todos.push({ id: 1, text: 'Learn Vue' })
// 由于是响应式对象,所有修改都会触发更新
响应式数据的深度处理
对于深层嵌套的对象,reactive会自动处理所有层级的响应式:
import { reactive } from 'vue'
const state = reactive({
user: {
profile: {
name: 'John',
settings: {
theme: 'dark',
language: 'en'
}
}
}
})
// 深层修改
state.user.profile.settings.theme = 'light'
// 这会触发响应式更新
ref vs reactive的使用场景
// 使用ref的场景
const count = ref(0) // 基本数据类型
const name = ref('Vue') // 字符串
const isVisible = ref(false) // 布尔值
// 使用reactive的场景
const state = reactive({
user: {
name: 'John',
age: 30
},
todos: [],
loading: false
})
// 复杂对象结构
const complexData = reactive({
items: [],
filters: {
category: '',
search: ''
},
pagination: {
page: 1,
limit: 10,
total: 0
}
})
组合函数复用
创建可复用的组合函数
组合函数是Composition API的核心优势之一,它允许我们将可复用的逻辑封装成函数:
// composables/useCounter.js
import { ref } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = initialValue
}
return {
count,
increment,
decrement,
reset
}
}
// composables/useFetch.js
import { ref, watch } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchData = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
watch(url, fetchData, { immediate: true })
return {
data,
loading,
error,
fetchData
}
}
组合函数的实际应用
// components/Counter.vue
import { defineComponent } from 'vue'
import { useCounter } from '@/composables/useCounter'
export default defineComponent({
name: 'Counter',
setup() {
const { count, increment, decrement, reset } = useCounter(0)
return {
count,
increment,
decrement,
reset
}
}
})
// components/UserList.vue
import { defineComponent } from 'vue'
import { useFetch } from '@/composables/useFetch'
export default defineComponent({
name: 'UserList',
setup() {
const { data: users, loading, error, fetchData } = useFetch('/api/users')
return {
users,
loading,
error,
fetchData
}
}
})
高级组合函数示例
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue)
// 从localStorage初始化
const initValue = localStorage.getItem(key)
if (initValue) {
try {
value.value = JSON.parse(initValue)
} catch (e) {
console.error('Failed to parse localStorage value:', e)
}
}
// 监听value变化并同步到localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value)
watch(value, (newValue) => {
const handler = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
return () => clearTimeout(handler)
})
return debouncedValue
}
组件通信
父子组件通信
在Composition API中,父子组件通信仍然保持简单直观:
// Parent.vue
import { defineComponent, ref } from 'vue'
import Child from './Child.vue'
export default defineComponent({
name: 'Parent',
setup() {
const message = ref('Hello from parent')
const count = ref(0)
const handleChildEvent = (data) => {
console.log('Received from child:', data)
count.value++
}
return {
message,
count,
handleChildEvent
}
},
components: {
Child
}
})
// Child.vue
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Child',
props: {
message: {
type: String,
default: ''
}
},
setup(props, { emit }) {
const handleClick = () => {
emit('child-event', { message: 'Hello from child', timestamp: Date.now() })
}
return {
handleClick
}
}
})
兄弟组件通信
对于兄弟组件之间的通信,可以使用组合函数来实现:
// composables/useEventBus.js
import { ref } from 'vue'
const events = ref({})
export function useEventBus() {
const on = (event, callback) => {
if (!events.value[event]) {
events.value[event] = []
}
events.value[event].push(callback)
}
const emit = (event, data) => {
if (events.value[event]) {
events.value[event].forEach(callback => callback(data))
}
}
const off = (event, callback) => {
if (events.value[event]) {
events.value[event] = events.value[event].filter(cb => cb !== callback)
}
}
return {
on,
emit,
off
}
}
// 使用示例
// ComponentA.vue
import { defineComponent } from 'vue'
import { useEventBus } from '@/composables/useEventBus'
export default defineComponent({
setup() {
const { emit } = useEventBus()
const sendMessage = () => {
emit('message-event', 'Hello from Component A')
}
return {
sendMessage
}
}
})
// ComponentB.vue
import { defineComponent } from 'vue'
import { useEventBus } from '@/composables/useEventBus'
export default defineComponent({
setup() {
const { on } = useEventBus()
const receivedMessage = ref('')
on('message-event', (data) => {
receivedMessage.value = data
})
return {
receivedMessage
}
}
})
复杂组件状态管理
多状态管理示例
// composables/useForm.js
import { ref, reactive, computed } from 'vue'
export function useForm(initialData = {}) {
const formData = reactive({ ...initialData })
const errors = ref({})
const isSubmitting = ref(false)
const isValid = computed(() => Object.keys(errors.value).length === 0)
const setField = (field, value) => {
formData[field] = value
// 清除对应字段的错误
if (errors.value[field]) {
delete errors.value[field]
}
}
const validateField = (field, value) => {
// 简单的验证规则
if (!value && field !== 'optionalField') {
errors.value[field] = `${field} is required`
return false
}
if (field === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
errors.value[field] = 'Email is invalid'
return false
}
delete errors.value[field]
return true
}
const validateAll = () => {
Object.keys(formData).forEach(field => {
validateField(field, formData[field])
})
return isValid.value
}
const submit = async (submitFn) => {
if (!validateAll()) return false
isSubmitting.value = true
try {
const result = await submitFn(formData)
return result
} catch (error) {
console.error('Form submission failed:', error)
return false
} finally {
isSubmitting.value = false
}
}
const reset = () => {
Object.keys(formData).forEach(key => {
formData[key] = initialData[key] || ''
})
errors.value = {}
}
return {
formData,
errors,
isSubmitting,
isValid,
setField,
validateField,
validateAll,
submit,
reset
}
}
状态管理的实际应用
<template>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label for="name">Name</label>
<input
id="name"
v-model="formData.name"
@blur="validateField('name', formData.name)"
:class="{ 'error': errors.name }"
>
<span v-if="errors.name" class="error-text">{{ errors.name }}</span>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
v-model="formData.email"
@blur="validateField('email', formData.email)"
:class="{ 'error': errors.email }"
>
<span v-if="errors.email" class="error-text">{{ errors.email }}</span>
</div>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? 'Submitting...' : 'Submit' }}
</button>
<div v-if="submitResult" class="submit-result">
{{ submitResult }}
</div>
</form>
</template>
<script>
import { defineComponent } from 'vue'
import { useForm } from '@/composables/useForm'
export default defineComponent({
name: 'UserForm',
setup() {
const {
formData,
errors,
isSubmitting,
setField,
validateField,
submit
} = useForm({
name: '',
email: ''
})
const submitHandler = async (data) => {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
return 'User created successfully!'
}
const handleSubmit = async () => {
const result = await submit(submitHandler)
if (result) {
submitResult.value = result
}
}
return {
formData,
errors,
isSubmitting,
setField,
validateField,
handleSubmit
}
}
})
</script>
高级特性和最佳实践
生命周期钩子的使用
Composition API提供了与Vue 2相同的生命周期钩子,但以函数形式提供:
import {
onMounted,
onUpdated,
onUnmounted,
onBeforeMount,
onBeforeUpdate,
onBeforeUnmount
} 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')
// 清理定时器、事件监听器等
})
return {}
}
}
响应式数据的高级用法
// 使用toRefs和toRef
import { ref, reactive, toRefs, toRef } from 'vue'
export default {
setup() {
const state = reactive({
name: 'Vue',
age: 30,
email: 'vue@example.com'
})
// 将响应式对象转换为ref
const { name, age, email } = toRefs(state)
// 或者单独转换某个属性
const nameRef = toRef(state, 'name')
return {
name,
age,
email,
nameRef
}
}
}
// 使用watchEffect
import { ref, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const doubleCount = ref(0)
// watchEffect会自动追踪依赖
watchEffect(() => {
doubleCount.value = count.value * 2
console.log('Count changed:', count.value)
})
return {
count,
doubleCount
}
}
}
性能优化技巧
// 使用computed的缓存
import { ref, computed } from 'vue'
export default {
setup() {
const items = ref([])
const filterText = ref('')
// 使用computed进行缓存
const filteredItems = computed(() => {
return items.value.filter(item =>
item.name.toLowerCase().includes(filterText.value.toLowerCase())
)
})
// 对于复杂计算,可以使用缓存
const expensiveValue = computed(() => {
// 模拟复杂计算
return items.value.reduce((acc, item) => {
return acc + item.value * 2
}, 0)
})
return {
items,
filterText,
filteredItems,
expensiveValue
}
}
}
// 使用memoization
import { ref, computed } from 'vue'
export function useMemoized(fn, deps) {
const cache = ref(null)
const lastDeps = ref(deps)
return computed(() => {
const shouldUpdate = !lastDeps.value.every((dep, index) => dep === deps[index])
if (shouldUpdate) {
cache.value = fn()
lastDeps.value = deps
}
return cache.value
})
}
实际项目案例
电商商品列表组件
<template>
<div class="product-list">
<div class="controls">
<input
v-model="searchQuery"
placeholder="Search products..."
class="search-input"
>
<select v-model="selectedCategory" class="category-select">
<option value="">All Categories</option>
<option v-for="category in categories" :key="category" :value="category">
{{ category }}
</option>
</select>
</div>
<div v-if="loading" class="loading">Loading...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<div v-else class="products-grid">
<ProductCard
v-for="product in filteredProducts"
:key="product.id"
:product="product"
@add-to-cart="handleAddToCart"
/>
</div>
<div class="pagination" v-if="totalPages > 1">
<button
@click="goToPage(currentPage - 1)"
:disabled="currentPage === 1"
>
Previous
</button>
<span>{{ currentPage }} of {{ totalPages }}</span>
<button
@click="goToPage(currentPage + 1)"
:disabled="currentPage === totalPages"
>
Next
</button>
</div>
</div>
</template>
<script>
import { defineComponent, ref, computed, watch } from 'vue'
import ProductCard from './ProductCard.vue'
import { useFetch } from '@/composables/useFetch'
import { useDebounce } from '@/composables/useDebounce'
export default defineComponent({
name: 'ProductList',
components: {
ProductCard
},
setup() {
const searchQuery = ref('')
const selectedCategory = ref('')
const currentPage = ref(1)
const pageSize = ref(12)
// 使用组合函数获取数据
const { data: products, loading, error } = useFetch(
() => `/api/products?page=${currentPage.value}&limit=${pageSize.value}`
)
const categories = computed(() => {
if (!products.value) return []
const uniqueCategories = [...new Set(products.value.map(p => p.category))]
return uniqueCategories.sort()
})
// 防抖搜索
const debouncedSearch = useDebounce(searchQuery, 300)
// 过滤产品
const filteredProducts = computed(() => {
if (!products.value) return []
let filtered = products.value
// 搜索过滤
if (debouncedSearch.value) {
filtered = filtered.filter(product =>
product.name.toLowerCase().includes(debouncedSearch.value.toLowerCase()) ||
product.description.toLowerCase().includes(debouncedSearch.value.toLowerCase())
)
}
// 分类过滤
if (selectedCategory.value) {
filtered = filtered.filter(product =>
product.category === selectedCategory.value
)
}
return filtered
})
const totalPages = computed(() => {
if (!products.value) return 1
return Math.ceil(filteredProducts.value.length / pageSize.value)
})
// 分页处理
const goToPage = (page) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
// 添加到购物车
const handleAddToCart = (product) => {
console.log('Added to cart:', product)
// 实际的购物车逻辑
}
// 监听搜索和分类变化
watch([debouncedSearch, selectedCategory], () => {
currentPage.value = 1
})
return {
searchQuery,
selectedCategory,
currentPage,
pageSize,
products,
loading,
error,
categories,
filteredProducts,
totalPages,
goToPage,
handleAddToCart
}
}
})
</script>
用户管理系统
<template>
<div class="user-management">
<div class="toolbar">
<button @click="showCreateForm = true" class="btn btn-primary">
Add User
</button>
<input
v-model="searchTerm"
placeholder="Search users..."
class="search-input"
>
</div>
<UserForm
v-if="showCreateForm"
@submit="handleCreateUser"
@cancel="showCreateForm = false"
/>
<div class="users-list">
<UserItem
v-for="user in filteredUsers"
:key="user.id"
:user="user"
@edit="handleEditUser"
@delete="handleDeleteUser"
/>
</div>
<div class="pagination">
<button
@click="currentPage--"
:disabled="currentPage === 1"
>
Previous
</button>
<span>{{ currentPage }} of {{ totalPages }}</span>
<button
@click="currentPage++"
:disabled="currentPage === totalPages"
>
Next
</button>
</div>
</div>
</template>
<script>
import { defineComponent, ref, computed, watch } from 'vue'
import UserForm from './UserForm.vue'
import UserItem from './UserItem.vue'
import { useFetch } from '@/composables/useFetch'
import { useLocalStorage } from '@/composables/useLocalStorage'
export default defineComponent({
name: 'UserManagement',
components: {
UserForm,
UserItem
},
setup() {
const searchTerm = ref('')
const currentPage = ref(1)
const pageSize = useLocalStorage('user-page-size', 10)
const showCreateForm = ref(false)
const { data: users, loading, error, fetchData } = useFetch('/api/users')
const filteredUsers = computed(() => {
if (!users.value) return []
let filtered = users.value
if (searchTerm.value) {
filtered = filtered.filter(user =>
user.name.toLowerCase().includes(searchTerm.value.toLowerCase()) ||
user.email.toLowerCase().includes(searchTerm.value.toLowerCase())
)
}
return filtered
})
const totalPages = computed(() => {
if (!filteredUsers.value) return 1
return Math.ceil(filteredUsers.value.length / pageSize.value)
})
const paginatedUsers = computed(() => {
if (!filteredUsers.value) return []
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return filteredUsers.value.slice(start, end)
})
const handleCreateUser = async (userData) => {
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(userData)
})
const newUser = await response.json()
// 更新用户列表
if (users.value) {
users.value.push(newUser)
}
showCreateForm.value = false
} catch (error) {
console.error('Failed to create user:', error)
}
}
const handleEditUser = (user) => {
// 编辑用户逻辑
console.log('Edit user:', user)
}
const handleDeleteUser = async (userId) => {
if (confirm('Are you sure you want to delete this user?')) {
try {
await fetch(`/api/users/${userId}`, {
method: 'DELETE'
})
// 从列表中移除用户
if (users.value) {
users.value = users.value.filter(user => user.id !== userId)
}
} catch (error) {
console.error('Failed to delete user:', error)
}
}
}
// 监听分页变化
watch([currentPage, pageSize], () => {
fetchData()
})
return {
searchTerm,
currentPage,
pageSize,
showCreateForm,
users,
loading,
error,
filteredUsers,
totalPages,
paginatedUsers,
handleCreateUser,
handleEditUser,
handleDeleteUser
}
}
})
</script>
最佳实践总结
代码组织原则
- 按功能分组:将相关的逻辑组织在一起,而不是按选项类型分组
- 组合函数复用:将可复用的逻辑封装成组合函数
- 清晰的返回值:明确地返回需要在模板中使用的变量和方法
性能优化建议
- 合理使用computed:利用计算属性的缓存机制
- 避免不必要的监听:使用watchEffect自动追踪依赖
- 及时清理资源:在组件销毁时清理定时器和事件监听器
开发工具和调试
// 开发环境下的调试工具
import { watch } from 'vue'
export function useDebug(name, data) {
if (process.env.NODE_ENV === 'development') {
watch(data, (newVal, oldVal) => {
console.log(`${name}
评论 (0)