引言
Vue 3 的发布带来了 Composition API 这一革命性的特性,它为开发者提供了更加灵活和强大的组件开发方式。相比传统的 Options API,Composition API 将组件的逻辑组织方式从"选项分类"转向了"逻辑组合",使得代码更加模块化、可复用和易于维护。
在现代前端开发中,组件状态管理与逻辑复用是构建复杂应用的核心挑战。Composition API 的出现为这些问题提供了优雅的解决方案。本文将深入探讨 Vue 3 Composition API 的高级用法,涵盖组件状态管理、逻辑复用、自定义 Hook 开发等核心概念,帮助开发者构建更灵活、可维护的 Vue 应用程序架构。
Composition API 核心概念
什么是 Composition API
Composition API 是 Vue 3 中引入的一种新的组件开发方式,它允许我们使用函数来组织和复用组件逻辑。与传统的 Options API(基于选项的对象配置)不同,Composition API 将相关的逻辑集中在一起,而不是按照功能类型分散在不同的选项中。
// 传统 Options API
export default {
data() {
return {
count: 0,
name: 'Vue'
}
},
methods: {
increment() {
this.count++
}
},
computed: {
greeting() {
return `Hello ${this.name}`
}
}
}
// Composition API
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue')
const increment = () => {
count.value++
}
const greeting = computed(() => `Hello ${name.value}`)
return {
count,
name,
increment,
greeting
}
}
}
setup 函数详解
setup 是 Composition API 的入口函数,它在组件实例创建之前执行,接收两个参数:props 和 context。
import { ref, reactive } from 'vue'
export default {
props: ['title'],
setup(props, context) {
// props 是响应式对象,可以访问传入的属性
console.log(props.title)
// context 包含组件上下文信息
const { attrs, slots, emit } = context
// 定义响应式数据
const count = ref(0)
const state = reactive({
name: 'Vue',
age: 3
})
// 返回的数据可以在模板中使用
return {
count,
state
}
}
}
组件状态管理
响应式数据的创建与使用
在 Composition API 中,我们主要通过 ref 和 reactive 来创建响应式数据。
Ref 的使用
ref 用于创建基本类型的响应式数据:
import { ref } from 'vue'
export default {
setup() {
// 创建基本类型响应式数据
const count = ref(0)
const message = ref('Hello Vue')
const isActive = ref(true)
// 访问和修改值
console.log(count.value) // 0
count.value = 10
console.log(count.value) // 10
return {
count,
message,
isActive
}
}
}
Reactive 的使用
reactive 用于创建对象类型的响应式数据:
import { reactive } from 'vue'
export default {
setup() {
// 创建响应式对象
const state = reactive({
count: 0,
name: 'Vue',
user: {
firstName: 'John',
lastName: 'Doe'
}
})
// 修改属性
state.count = 10
state.user.firstName = 'Jane'
return {
state
}
}
}
响应式数据的深层嵌套处理
对于深层嵌套的对象,我们需要特别注意响应式的处理:
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
user: {
profile: {
name: 'John',
age: 30,
address: {
city: 'New York',
country: 'USA'
}
}
}
})
// 使用 toRefs 可以将响应式对象的属性转换为 ref
const { user } = toRefs(state)
return {
state,
user
}
}
}
计算属性与监听器
计算属性的创建
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
const age = ref(30)
// 基本计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 带 getter 和 setter 的计算属性
const reversedName = computed({
get: () => firstName.value.split('').reverse().join(''),
set: (value) => {
firstName.value = value.split('').reverse().join('')
}
})
// 基于多个依赖的计算属性
const isAdult = computed(() => age.value >= 18)
return {
firstName,
lastName,
age,
fullName,
reversedName,
isAdult
}
}
}
监听器的使用
import { ref, watch, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const name = ref('Vue')
// 基本监听器
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})
// 监听多个源
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} -> ${newCount}, name: ${oldName} -> ${newName}`)
})
// 深度监听
const state = ref({
user: {
name: 'John'
}
})
watch(state, (newValue) => {
console.log('state changed:', newValue)
}, { deep: true })
// watchEffect 会自动追踪其内部使用的响应式数据
watchEffect(() => {
console.log(`Current count is: ${count.value}`)
})
return {
count,
name
}
}
}
逻辑复用与自定义 Hook
创建可复用的逻辑组合
Composition API 的核心优势在于逻辑复用。通过创建自定义的组合函数,我们可以将通用的逻辑封装起来,在多个组件中重复使用。
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = initialValue
}
const doubleCount = computed(() => count.value * 2)
return {
count,
increment,
decrement,
reset,
doubleCount
}
}
// composables/useFetch.js
import { ref, reactive } from 'vue'
export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const fetchData = async () => {
try {
loading.value = true
error.value = null
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData
}
}
在组件中使用自定义 Hook
<template>
<div>
<h2>Counter Example</h2>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
<h2>Fetch Example</h2>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else-if="data">
<pre>{{ JSON.stringify(data, null, 2) }}</pre>
</div>
<button @click="fetchData">Fetch Data</button>
</div>
</template>
<script>
import { useCounter } from '@/composables/useCounter'
import { useFetch } from '@/composables/useFetch'
export default {
setup() {
// 使用自定义 Hook
const { count, increment, decrement, reset, doubleCount } = useCounter(10)
const { data, loading, error, fetchData } = useFetch('https://api.example.com/data')
return {
count,
increment,
decrement,
reset,
doubleCount,
data,
loading,
error,
fetchData
}
}
}
</script>
高级逻辑复用模式
带参数的自定义 Hook
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue)
// 从 localStorage 初始化值
const storedValue = localStorage.getItem(key)
if (storedValue) {
try {
value.value = JSON.parse(storedValue)
} catch (error) {
console.error('Failed to parse localStorage value:', error)
}
}
// 监听值变化并同步到 localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
// 使用示例
export default {
setup() {
const theme = useLocalStorage('theme', 'light')
const preferences = useLocalStorage('user-preferences', {})
return {
theme,
preferences
}
}
}
带副作用的 Hook
// composables/useWindowResize.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useWindowResize() {
const width = ref(window.innerWidth)
const height = ref(window.innerHeight)
const handleResize = () => {
width.value = window.innerWidth
height.value = window.innerHeight
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
return {
width,
height
}
}
// composables/useInterval.js
import { ref, onMounted, onUnmounted } from 'vue'
export function useInterval(callback, delay) {
const intervalId = ref(null)
onMounted(() => {
if (delay !== null) {
intervalId.value = setInterval(callback, delay)
}
})
onUnmounted(() => {
if (intervalId.value) {
clearInterval(intervalId.value)
}
})
return {
intervalId
}
}
组件通信与状态管理
父子组件通信
<!-- Parent.vue -->
<template>
<div>
<h2>Parent Component</h2>
<Child :message="parentMessage" @child-event="handleChildEvent" />
<p>Received from child: {{ childMessage }}</p>
</div>
</template>
<script>
import { ref } from 'vue'
import Child from './Child.vue'
export default {
components: {
Child
},
setup() {
const parentMessage = ref('Hello from Parent')
const childMessage = ref('')
const handleChildEvent = (message) => {
childMessage.value = message
}
return {
parentMessage,
childMessage,
handleChildEvent
}
}
}
</script>
<!-- Child.vue -->
<template>
<div>
<h3>Child Component</h3>
<p>{{ message }}</p>
<button @click="emitEvent">Send to Parent</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
props: ['message'],
setup(props, { emit }) {
const emitEvent = () => {
emit('child-event', 'Hello from Child')
}
return {
emitEvent
}
}
}
</script>
全局状态管理
对于需要跨多个组件共享的状态,我们可以创建全局的响应式状态:
// stores/globalStore.js
import { reactive } from 'vue'
export const globalStore = reactive({
user: null,
theme: 'light',
language: 'en',
notifications: []
})
export function setUser(user) {
globalStore.user = user
}
export function setTheme(theme) {
globalStore.theme = theme
}
export function addNotification(notification) {
globalStore.notifications.push({
id: Date.now(),
...notification,
timestamp: new Date()
})
}
export function removeNotification(id) {
const index = globalStore.notifications.findIndex(n => n.id === id)
if (index > -1) {
globalStore.notifications.splice(index, 1)
}
}
<!-- App.vue -->
<template>
<div :class="`app ${theme}`">
<header>
<h1>Vue 3 App</h1>
<nav>
<button @click="toggleTheme">Toggle Theme</button>
</nav>
</header>
<main>
<router-view />
</main>
<footer>
<div v-for="notification in notifications" :key="notification.id">
{{ notification.message }}
</div>
</footer>
</div>
</template>
<script>
import { computed } from 'vue'
import { globalStore, setTheme } from '@/stores/globalStore'
export default {
setup() {
const theme = computed(() => globalStore.theme)
const notifications = computed(() => globalStore.notifications)
const toggleTheme = () => {
setTheme(globalStore.theme === 'light' ? 'dark' : 'light')
}
return {
theme,
notifications,
toggleTheme
}
}
}
</script>
性能优化与最佳实践
计算属性的优化
import { ref, computed } from 'vue'
export default {
setup() {
const items = ref([])
// 避免在计算属性中执行昂贵的操作
const expensiveComputed = computed(() => {
// 如果这个操作很昂贵,考虑使用缓存策略
return items.value.map(item => {
// 模拟昂贵的计算
return item.value * 2 + Math.random()
})
})
// 使用缓存避免重复计算
const cachedResult = computed(() => {
// 只有当依赖项改变时才重新计算
return items.value.reduce((acc, item) => acc + item.value, 0)
})
return {
expensiveComputed,
cachedResult
}
}
}
组件性能监控
// composables/usePerformance.js
import { ref, onMounted, onUnmounted } from 'vue'
export function usePerformance() {
const performanceData = ref({
renderTime: 0,
memoryUsage: 0,
fps: 0
})
let animationFrameId = null
const startPerformanceMonitoring = () => {
if (performance) {
// 监控渲染时间
const startTime = performance.now()
const monitor = () => {
const endTime = performance.now()
performanceData.value.renderTime = endTime - startTime
// 每秒更新一次 FPS
performanceData.value.fps = Math.round(1000 / (endTime - startTime))
animationFrameId = requestAnimationFrame(monitor)
}
animationFrameId = requestAnimationFrame(monitor)
}
}
const stopPerformanceMonitoring = () => {
if (animationFrameId) {
cancelAnimationFrame(animationFrameId)
}
}
onMounted(() => {
startPerformanceMonitoring()
})
onUnmounted(() => {
stopPerformanceMonitoring()
})
return {
performanceData
}
}
内存泄漏预防
// composables/useEventListeners.js
import { onUnmounted } from 'vue'
export function useEventListeners(target, events) {
const listeners = []
// 添加事件监听器
const addListener = (event, handler) => {
target.addEventListener(event, handler)
listeners.push({ event, handler })
}
// 移除所有事件监听器
const removeAllListeners = () => {
listeners.forEach(({ event, handler }) => {
target.removeEventListener(event, handler)
})
listeners.length = 0
}
onUnmounted(() => {
removeAllListeners()
})
return {
addListener
}
}
// 使用示例
export default {
setup() {
const { addListener } = useEventListeners(window, [])
// 在组件挂载时添加监听器
addListener('resize', () => {
console.log('Window resized')
})
return {}
}
}
高级特性与实用技巧
异步操作处理
import { ref, reactive } from 'vue'
export default {
setup() {
const loading = ref(false)
const error = ref(null)
const data = ref(null)
// 处理异步操作的通用函数
const asyncHandler = async (asyncFunction) => {
try {
loading.value = true
error.value = null
data.value = await asyncFunction()
} catch (err) {
error.value = err.message
console.error('Async operation failed:', err)
} finally {
loading.value = false
}
}
const fetchData = () => {
return asyncHandler(async () => {
const response = await fetch('/api/data')
return response.json()
})
}
return {
loading,
error,
data,
fetchData
}
}
}
条件渲染与动态组件
<template>
<div>
<component :is="currentComponent" />
<button @click="switchComponent">Switch Component</button>
</div>
</template>
<script>
import { ref, defineAsyncComponent } from 'vue'
export default {
setup() {
const currentComponent = ref('ComponentA')
// 动态导入组件
const ComponentA = defineAsyncComponent(() => import('./ComponentA.vue'))
const ComponentB = defineAsyncComponent(() => import('./ComponentB.vue'))
const switchComponent = () => {
currentComponent.value = currentComponent.value === 'ComponentA' ? 'ComponentB' : 'ComponentA'
}
return {
currentComponent,
switchComponent
}
}
}
</script>
混合模式与继承
// mixins/baseMixin.js
import { ref, computed } from 'vue'
export const baseMixin = {
setup() {
const name = ref('Base Component')
const version = ref('1.0.0')
const displayName = computed(() => `${name.value} v${version.value}`)
return {
name,
version,
displayName
}
}
}
// 组件中使用混合
import { baseMixin } from '@/mixins/baseMixin'
export default {
mixins: [baseMixin],
setup(props, context) {
// 可以访问 mixin 中定义的响应式数据和计算属性
const { name, version, displayName } = baseMixin.setup()
return {
name,
version,
displayName
}
}
}
实际项目应用案例
复杂表单处理
<template>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>Username:</label>
<input v-model="formData.username" type="text" />
</div>
<div class="form-group">
<label>Email:</label>
<input v-model="formData.email" type="email" />
</div>
<div class="form-group">
<label>Password:</label>
<input v-model="formData.password" type="password" />
</div>
<div class="form-group">
<label>Confirm Password:</label>
<input v-model="formData.confirmPassword" type="password" />
</div>
<button type="submit" :disabled="loading">Submit</button>
<div v-if="error" class="error">{{ error }}</div>
</form>
</template>
<script>
import { reactive, computed } from 'vue'
import { useValidation } from '@/composables/useValidation'
export default {
setup() {
const formData = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const { validateForm, errors } = useValidation(formData)
const isFormValid = computed(() => {
return Object.keys(errors.value).length === 0
})
const loading = ref(false)
const error = ref('')
const handleSubmit = async () => {
if (!isFormValid.value) {
return
}
try {
loading.value = true
error.value = ''
// 提交表单数据
await submitForm(formData)
console.log('Form submitted successfully')
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
formData,
loading,
error,
handleSubmit
}
}
}
</script>
数据表格组件
<template>
<div class="data-table">
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column.key" @click="sort(column.key)">
{{ column.title }}
<span v-if="sortBy === column.key" class="sort-indicator">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in paginatedData" :key="row.id">
<td v-for="column in columns" :key="column.key">
{{ row[column.key] }}
</td>
</tr>
</tbody>
</table>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">Previous</button>
<span>Page {{ currentPage }} of {{ totalPages }}</span>
<button @click="nextPage" :disabled="currentPage === totalPages">Next</button>
</div>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue'
export default {
props: {
data: {
type: Array,
required: true
},
columns: {
type: Array,
required: true
},
pageSize: {
type: Number,
default: 10
}
},
setup(props) {
const currentPage = ref(1)
const sortBy = ref('')
const sortOrder = ref('asc')
const sortedData = computed(() => {
if (!sortBy.value) return props.data
return [...props.data].sort((a, b) => {
const aValue = a[sortBy.value]
const bValue = b[sortBy.value]
if (aValue < bValue) return sortOrder.value === 'asc' ? -1 : 1
if (aValue > bValue) return sortOrder.value === 'asc' ? 1 : -1
return 0
})
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * props.pageSize
const end = start + props.pageSize
return sortedData.value.slice(start, end)
})
const totalPages = computed(() => {
return Math.ceil(props.data.length / props.pageSize)
})
const sort = (key) => {
if (sortBy.value === key) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortBy.value = key
sortOrder.value = 'asc'
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
// 监听数据变化重置分页
watch(() => props.data, () => {
currentPage.value = 1
})
return {
currentPage,
sortBy,
sortOrder,
paginatedData,
totalPages,
sort,
nextPage,
prevPage
}
}
}
</script>
总结与展望
Vue 3 的 Composition API 为前端开发带来了革命性的变化,它让组件的逻辑组织更加灵活和直观。通过本文的详细介绍,我们可以看到 Composition API 在以下方面具有显著优势:
- 更好的逻辑复用:通过自定义 Hook,我们可以将通用逻辑封装起来,在多个组件中重复使用
- 更清晰的状态管理:响应式数据的创建和管理变得更加直观和可控
- 更强的类型支持:结合 TypeScript,可以提供更好的开发体验和错误检查
- 更高的性能:通过合理的优化策略,可以有效避免不必要的计算和渲染
随着 Vue 3 的不断发展,Composition API 也在不断完善。未来的版本可能会带来更多高级特性,如更强大的组合函数、更好的调试工具等。开发者应该积极拥抱这些变化,利用 Composition API 的优势来构建更加优雅和高效的前端应用。
在实际项目中,建议根据具体需求选择合适的 API 方式。对于简单组件,传统的 Options API 仍然适用;而对于复杂的业务逻辑,Composition API 能够提供更好的解决方案。最重要的是,在团队开发中保持一致的编码规范,确保代码的可维护性和可读性。
通过深入理解和掌握 Composition API 的最佳实践,开发者可以构建出更加健壮、可维护和高效的 Vue 应用程序,为用户提供更好的用户体验。

评论 (0)