引言
Vue 3 的发布带来了革命性的变化,其中最引人注目的就是 Composition API 的引入。作为 Vue 3 的核心特性之一,Composition API 为开发者提供了更加灵活和强大的组件状态管理方式。相比于传统的 Options API,Composition API 更加注重逻辑复用和代码组织,使得复杂的组件开发变得更加清晰和可维护。
本文将深入探讨 Vue 3 Composition API 的各个方面,从基础概念到高级应用,通过实际项目案例演示如何构建复杂的组件状态管理和逻辑复用。无论你是 Vue 2 的开发者还是初学者,都能在这篇文章中找到有价值的内容。
什么是 Composition API
核心理念
Composition API 是 Vue 3 中引入的一种新的组件开发模式,它允许我们将组件的逻辑以函数的形式组织和重用。与 Options API(Vue 2 中的传统方式)不同,Composition API 不再依赖于预定义的选项(如 data、methods、computed 等),而是通过组合不同的函数来构建组件。
主要优势
- 更好的逻辑复用:通过组合函数实现跨组件的逻辑共享
- 更灵活的代码组织:按照功能而不是选项类型来组织代码
- 更强的类型支持:在 TypeScript 中提供更好的类型推断
- 更清晰的组件结构:减少重复代码,提高可读性
基础概念与核心函数
setup 函数
setup 是 Composition API 的入口函数,它在组件实例创建之前执行。在这个函数中,我们可以访问组件的所有响应式数据和方法。
import { ref, reactive } from 'vue'
export default {
setup() {
// 响应式数据
const count = ref(0)
const user = reactive({
name: 'John',
age: 30
})
// 方法
const increment = () => {
count.value++
}
// 返回的数据和方法将在模板中使用
return {
count,
user,
increment
}
}
}
响应式数据
Composition API 提供了多种创建响应式数据的方法:
import { ref, reactive, computed, watch } from 'vue'
export default {
setup() {
// 创建基本响应式变量
const count = ref(0)
// 创建响应式对象
const state = reactive({
name: 'Vue',
version: '3.0'
})
// 创建计算属性
const doubleCount = computed(() => count.value * 2)
// 创建监听器
watch(count, (newVal, oldVal) => {
console.log(`count changed from ${oldVal} to ${newVal}`)
})
return {
count,
state,
doubleCount
}
}
}
基础实战:创建一个计数器组件
让我们从一个简单的计数器开始,展示如何使用 Composition API 构建基础组件。
<template>
<div class="counter">
<h2>计数器</h2>
<p>当前值: {{ count }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'Counter',
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = 0
}
return {
count,
increment,
decrement,
reset
}
}
}
</script>
<style scoped>
.counter {
padding: 20px;
border: 1px solid #ccc;
border-radius: 4px;
text-align: center;
}
button {
margin: 5px;
padding: 8px 16px;
cursor: pointer;
}
</style>
高级特性:响应式数据管理
使用 reactive 和 ref 的区别
import { ref, reactive } from 'vue'
export default {
setup() {
// ref 用于基本类型和对象的包装
const count = ref(0)
const message = ref('Hello')
// reactive 用于创建响应式对象
const user = reactive({
name: 'John',
age: 30,
address: {
city: 'Beijing',
country: 'China'
}
})
// 在模板中使用时,ref 需要访问 .value
const increment = () => {
count.value++
}
// reactive 对象可以直接访问属性
const updateAddress = () => {
user.address.city = 'Shanghai'
}
return {
count,
message,
user,
increment,
updateAddress
}
}
}
计算属性和监听器
<template>
<div class="advanced-counter">
<h2>高级计数器</h2>
<p>原始值: {{ count }}</p>
<p>双倍值: {{ doubleCount }}</p>
<p>平方值: {{ squareCount }}</p>
<p>状态: {{ status }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue'
export default {
setup() {
const count = ref(0)
// 计算属性
const doubleCount = computed(() => count.value * 2)
const squareCount = computed(() => count.value * count.value)
// 基于计算属性的状态
const status = computed(() => {
if (count.value < 0) return '负数'
if (count.value === 0) return '零'
if (count.value > 100) return '过大'
return '正常'
})
// 监听器
watch(count, (newVal, oldVal) => {
console.log(`计数从 ${oldVal} 变为 ${newVal}`)
})
// 监听多个值
watch([count], ([newCount], [oldCount]) => {
if (newCount > 10 && oldCount <= 10) {
console.log('超过10了!')
}
})
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
const reset = () => {
count.value = 0
}
return {
count,
doubleCount,
squareCount,
status,
increment,
decrement,
reset
}
}
}
</script>
逻辑复用:组合函数(Composables)
创建可复用的组合函数
// 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 double = computed(() => count.value * 2)
return {
count,
increment,
decrement,
reset,
double
}
}
// composables/useLocalStorage.js
import { ref, watch } from 'vue'
export function useLocalStorage(key, defaultValue) {
const value = ref(defaultValue)
// 初始化时从 localStorage 中读取
const savedValue = localStorage.getItem(key)
if (savedValue) {
value.value = JSON.parse(savedValue)
}
// 监听值变化并保存到 localStorage
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
使用组合函数
<template>
<div class="composable-demo">
<h2>组合函数演示</h2>
<!-- 计数器 -->
<div class="counter-section">
<h3>计数器</h3>
<p>值: {{ counter.count }}</p>
<p>双倍: {{ counter.double }}</p>
<button @click="counter.increment">增加</button>
<button @click="counter.decrement">减少</button>
<button @click="counter.reset">重置</button>
</div>
<!-- 本地存储 -->
<div class="storage-section">
<h3>本地存储</h3>
<input v-model="storageValue" placeholder="输入内容">
<p>保存的值: {{ storageValue }}</p>
</div>
</div>
</template>
<script>
import { useCounter } from '@/composables/useCounter'
import { useLocalStorage } from '@/composables/useLocalStorage'
export default {
setup() {
const counter = useCounter(10)
const storageValue = useLocalStorage('demo-value', '默认值')
return {
counter,
storageValue
}
}
}
</script>
复杂组件状态管理
表单处理组合函数
// composables/useForm.js
import { reactive, readonly } from 'vue'
export function useForm(initialData = {}) {
const formData = reactive({ ...initialData })
const errors = reactive({})
const isSubmitting = ref(false)
const validateField = (field, value) => {
// 简单的验证规则
if (!value && field !== 'optional') {
errors[field] = `${field} 是必填项`
return false
}
if (field === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
errors[field] = '请输入有效的邮箱地址'
return false
}
delete errors[field]
return true
}
const validateAll = () => {
let isValid = true
Object.keys(formData).forEach(field => {
if (!validateField(field, formData[field])) {
isValid = false
}
})
return isValid
}
const setFieldValue = (field, value) => {
formData[field] = value
validateField(field, value)
}
const submit = async (submitHandler) => {
if (!validateAll()) return false
isSubmitting.value = true
try {
await submitHandler(formData)
return true
} catch (error) {
console.error('提交失败:', error)
return false
} finally {
isSubmitting.value = false
}
}
const reset = () => {
Object.keys(formData).forEach(key => {
formData[key] = initialData[key] || ''
})
Object.keys(errors).forEach(key => {
delete errors[key]
})
}
return readonly({
formData,
errors,
isSubmitting,
setFieldValue,
validateAll,
submit,
reset
})
}
表单组件实现
<template>
<div class="form-demo">
<h2>表单演示</h2>
<form @submit.prevent="handleSubmit">
<div class="form-group">
<label>姓名:</label>
<input
v-model="form.formData.name"
type="text"
:class="{ error: form.errors.name }"
>
<span v-if="form.errors.name" class="error-message">{{ form.errors.name }}</span>
</div>
<div class="form-group">
<label>邮箱:</label>
<input
v-model="form.formData.email"
type="email"
:class="{ error: form.errors.email }"
>
<span v-if="form.errors.email" class="error-message">{{ form.errors.email }}</span>
</div>
<div class="form-group">
<label>年龄:</label>
<input
v-model.number="form.formData.age"
type="number"
:class="{ error: form.errors.age }"
>
<span v-if="form.errors.age" class="error-message">{{ form.errors.age }}</span>
</div>
<div class="form-group">
<label>描述:</label>
<textarea
v-model="form.formData.description"
:class="{ error: form.errors.description }"
></textarea>
<span v-if="form.errors.description" class="error-message">{{ form.errors.description }}</span>
</div>
<button type="submit" :disabled="form.isSubmitting">
{{ form.isSubmitting ? '提交中...' : '提交' }}
</button>
</form>
<div class="result">
<h3>当前表单数据:</h3>
<pre>{{ JSON.stringify(form.formData, null, 2) }}</pre>
</div>
</div>
</template>
<script>
import { useForm } from '@/composables/useForm'
export default {
setup() {
const initialData = {
name: '',
email: '',
age: null,
description: ''
}
const form = useForm(initialData)
const handleSubmit = async () => {
const success = await form.submit(async (data) => {
// 模拟异步提交
await new Promise(resolve => setTimeout(resolve, 1000))
console.log('表单提交成功:', data)
alert('表单提交成功!')
})
if (!success) {
console.log('表单验证失败')
}
}
return {
form,
handleSubmit
}
}
}
</script>
<style scoped>
.form-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
input, textarea {
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
input.error, textarea.error {
border-color: #ff0000;
}
.error-message {
color: #ff0000;
font-size: 12px;
margin-top: 5px;
display: block;
}
button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.result {
margin-top: 30px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 4px;
}
</style>
异步数据处理
API 请求组合函数
// composables/useApi.js
import { ref, readonly } from 'vue'
export function useApi(apiFunction) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const execute = async (...args) => {
try {
loading.value = true
error.value = null
data.value = await apiFunction(...args)
} catch (err) {
error.value = err.message || '请求失败'
console.error('API 请求错误:', err)
} finally {
loading.value = false
}
}
const reset = () => {
data.value = null
error.value = null
loading.value = false
}
return readonly({
data,
loading,
error,
execute,
reset
})
}
// composables/usePagination.js
import { ref, computed, watch } from 'vue'
export function usePagination(initialPage = 1, initialPageSize = 10) {
const page = ref(initialPage)
const pageSize = ref(initialPageSize)
const total = ref(0)
const totalPages = computed(() => {
return Math.ceil(total.value / pageSize.value)
})
const hasPrev = computed(() => {
return page.value > 1
})
const hasNext = computed(() => {
return page.value < totalPages.value
})
const nextPage = () => {
if (hasNext.value) {
page.value++
}
}
const prevPage = () => {
if (hasPrev.value) {
page.value--
}
}
const goToPage = (newPage) => {
if (newPage >= 1 && newPage <= totalPages.value) {
page.value = newPage
}
}
const setPageSize = (size) => {
pageSize.value = size
page.value = 1 // 重置到第一页
}
watch([page, pageSize], () => {
// 可以在这里添加分页变化的处理逻辑
})
return {
page,
pageSize,
total,
totalPages,
hasPrev,
hasNext,
nextPage,
prevPage,
goToPage,
setPageSize,
setPage: (newPage) => page.value = newPage
}
}
数据列表组件
<template>
<div class="data-list">
<h2>数据列表</h2>
<!-- 加载状态 -->
<div v-if="loading" class="loading">加载中...</div>
<!-- 错误处理 -->
<div v-else-if="error" class="error">{{ error }}</div>
<!-- 数据列表 -->
<div v-else>
<div class="pagination-controls">
<button @click="prevPage" :disabled="!hasPrev">上一页</button>
<span>第 {{ page }} 页 / 共 {{ totalPages }} 页</span>
<button @click="nextPage" :disabled="!hasNext">下一页</button>
</div>
<ul class="data-list-items">
<li v-for="item in data" :key="item.id" class="data-item">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
<small>ID: {{ item.id }}</small>
</li>
</ul>
<div class="pagination-controls">
<button @click="prevPage" :disabled="!hasPrev">上一页</button>
<span>第 {{ page }} 页 / 共 {{ totalPages }} 页</span>
<button @click="nextPage" :disabled="!hasNext">下一页</button>
</div>
</div>
</div>
</template>
<script>
import { useApi } from '@/composables/useApi'
import { usePagination } from '@/composables/usePagination'
// 模拟 API 请求
const fetchItems = async (page, pageSize) => {
// 模拟异步请求
await new Promise(resolve => setTimeout(resolve, 1000))
const start = (page - 1) * pageSize
const items = []
for (let i = 0; i < pageSize; i++) {
items.push({
id: start + i + 1,
title: `项目 ${start + i + 1}`,
content: `这是第 ${start + i + 1} 个项目的描述内容`
})
}
return {
items,
total: 100
}
}
export default {
setup() {
const { data, loading, error, execute } = useApi(fetchItems)
const pagination = usePagination(1, 10)
// 监听分页变化并重新加载数据
const loadData = async () => {
const result = await execute(pagination.page.value, pagination.pageSize.value)
if (result) {
pagination.total.value = result.total
}
}
// 初始化加载
loadData()
// 监听分页变化
const watchPage = () => {
loadData()
}
// 使用 watch 监听分页参数变化
watch([pagination.page, pagination.pageSize], watchPage)
return {
data: data.value,
loading: loading.value,
error: error.value,
...pagination
}
}
}
</script>
<style scoped>
.data-list {
padding: 20px;
}
.loading, .error {
text-align: center;
padding: 20px;
}
.error {
color: #ff0000;
background-color: #ffebee;
border-radius: 4px;
}
.pagination-controls {
display: flex;
justify-content: center;
align-items: center;
margin: 20px 0;
gap: 10px;
}
.data-list-items {
list-style: none;
padding: 0;
margin: 0;
}
.data-item {
border: 1px solid #eee;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
.data-item h3 {
margin: 0 0 10px 0;
color: #333;
}
.data-item p {
margin: 0 0 10px 0;
color: #666;
}
button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
</style>
复杂状态管理:多组件通信
状态管理组合函数
// composables/useGlobalState.js
import { reactive, readonly } from 'vue'
// 全局状态存储
const globalState = reactive({
user: null,
theme: 'light',
language: 'zh-CN'
})
// 状态变更函数
export function useGlobalState() {
const setUser = (user) => {
globalState.user = user
}
const setTheme = (theme) => {
globalState.theme = theme
}
const setLanguage = (language) => {
globalState.language = language
}
const logout = () => {
globalState.user = null
}
return readonly({
state: globalState,
setUser,
setTheme,
setLanguage,
logout
})
}
// composables/useEventBus.js
import { ref } from 'vue'
export function useEventBus() {
const events = ref({})
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
}
}
状态管理组件示例
<template>
<div class="global-state-demo">
<h2>全局状态管理演示</h2>
<!-- 用户信息 -->
<div class="user-info" v-if="globalState.state.user">
<h3>用户信息</h3>
<p>姓名: {{ globalState.state.user.name }}</p>
<p>邮箱: {{ globalState.state.user.email }}</p>
<button @click="logout">退出登录</button>
</div>
<!-- 登录表单 -->
<div class="login-form" v-else>
<h3>用户登录</h3>
<form @submit.prevent="handleLogin">
<input
v-model="loginForm.email"
type="email"
placeholder="邮箱"
required
>
<input
v-model="loginForm.password"
type="password"
placeholder="密码"
required
>
<button type="submit">登录</button>
</form>
</div>
<!-- 主题切换 -->
<div class="theme-controls">
<h3>主题设置</h3>
<button @click="setTheme('light')">浅色主题</button>
<button @click="setTheme('dark')">深色主题</button>
<p>当前主题: {{ globalState.state.theme }}</p>
</div>
<!-- 事件总线演示 -->
<div class="event-bus-demo">
<h3>事件总线演示</h3>
<button @click="emitEvent">发送事件</button>
<p>收到的事件: {{ eventMessage }}</p>
</div>
</div>
</template>
<script>
import { useGlobalState } from '@/composables/useGlobalState'
import { useEventBus } from '@/composables/useEventBus'
export default {
setup() {
const globalState = useGlobalState()
const eventBus = useEventBus()
const loginForm = {
email: '',
password: ''
}
const eventMessage = ref('')
// 监听事件
eventBus.on('demo-event', (data) => {
eventMessage.value = `收到事件: ${JSON.stringify(data)}`
})
const handleLogin = async () => {
// 模拟登录
await new Promise(resolve => setTimeout(resolve, 1000))
globalState.setUser({
name: '张三',
email: loginForm.email
})
loginForm.email = ''
loginForm.password = ''
}
const logout = () => {
globalState.logout()
}
const setTheme = (theme) => {
globalState.setTheme(theme)
}
const emitEvent = () => {
eventBus.emit('demo-event', {
message: 'Hello from event bus!',
timestamp: new Date().toISOString()
})
}
return {
globalState,
loginForm,
eventMessage,
handleLogin,
logout,
setTheme,
emitEvent
}
}
}
</script>
<style scoped>
.global-state-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.user-info, .login-form, .theme-controls, .event-bus-demo {
border: 1px solid #eee;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
form {
display: flex;
flex
评论 (0)