Vue 3 组合式 API 深度指南
Vue 3 的组合式 API 改变了我们组织 Vue 代码的方式。本文深度讲解其核心概念和实战用法。
1. 为什么要使用组合式 API
选项式 API 的问题
<script>
// ❌ 选项式 API 的问题:
// 1. 逻辑分散在 data、methods、computed 等不同地方
// 2. 难以复用逻辑
// 3. 大型组件难以维护
export default {
data() {
return {
count: 0,
name: '',
users: []
}
},
computed: {
double() { return this.count * 2 },
isAdult() { return this.age >= 18 }
},
methods: {
increment() { this.count++ },
setName(name) { this.name = name },
async fetchUsers() { /* ... */ }
},
watch: {
count(newVal) { console.log('count changed') }
}
}
</script>
组合式 API 的优势
<script setup lang="ts">
// ✅ 组合式 API 的优势:
// 1. 逻辑聚集在一起
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
watch(count, (newVal) => {
console.log('count changed:', newVal)
})
// 2. 易于提取为 composable 复用
// 3. 更好的 TypeScript 支持
// 4. 更小的包体积
// 5. 更灵活的逻辑组织
// 相同功能的代码量通常减少 30-50%
</script>
2. 核心响应式 API
ref: 简单值的响应式
import { ref } from 'vue'
// 创建响应式简单值
const count = ref(0)
const name = ref('Alice')
const isActive = ref(false)
// ❌ 错误: ref 需要 .value 访问
console.log(count) // { value: 0 }
// ✅ 正确: 在 JavaScript 中需要 .value
console.log(count.value) // 0
// ✅ 在模板中自动解包
<template>
{{ count }} <!-- 自动为 0,无需 .value -->
{{ count + 1 }} <!-- 直接计算 -->
</template>
// 响应式更新
const increment = () => {
count.value++ // 必须使用 .value
}
reactive: 对象的深度响应式
import { reactive } from 'vue'
// 创建响应式对象
const user = reactive({
id: 1,
name: 'Alice',
email: 'alice@example.com',
address: {
city: 'Beijing',
zip: '100000'
}
})
// 无需 .value,直接访问和修改
console.log(user.name) // "Alice"
user.name = 'Bob' // 更新
user.address.city = 'Shanghai' // 深度响应式
// ✅ 可以直接解构
const { name, email } = user // 有效(reactive)
// ❌ 不要对 ref 解构(会失去响应性)
const count = ref(0)
const { value } = count // ❌ value 不再响应式
computed: 计算属性
import { ref, computed } from 'vue'
const count = ref(0)
// 1. 只读计算属性
const double = computed(() => count.value * 2)
console.log(double.value) // 0 时为 0
// 2. 可写计算属性
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (value: string) => {
const [first, last] = value.split(' ')
firstName.value = first
lastName.value = last
}
})
fullName.value = 'John Doe' // 触发 setter
watch: 侦听器
import { ref, watch } from 'vue'
const count = ref(0)
const user = reactive({ name: 'Alice', age: 0 })
// 1. 侦听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变为 ${newVal}`)
})
// 2. 侦听多个源
watch(
[count, () => user.name],
([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount} → ${newCount}`)
console.log(`name: ${oldName} → ${newName}`)
}
)
// 3. 深度侦听 (监听对象的嵌套属性)
watch(
() => user,
(newUser) => {
console.log('user 对象改变:', newUser)
},
{ deep: true } // 深度侦听
)
// 4. 立即执行
watch(count, (newVal) => {
console.log('立即执行,且每次 count 改变时执行')
}, { immediate: true })
// 5. 配置侦听时机
watch(count, (newVal) => {
console.log('在组件更新后执行')
}, { flush: 'post' })
// 停止侦听
const unwatch = watch(count, () => { /* ... */ })
unwatch() // 停止侦听
3. 生命周期 Hooks
生命周期钩子对应关系
import {
onBeforeMount, onMounted,
onBeforeUpdate, onUpdated,
onBeforeUnmount, onUnmounted,
onErrorCaptured, onActivated, onDeactivated
} from 'vue'
// 挂载
onBeforeMount(() => console.log('组件挂载前'))
onMounted(() => console.log('组件挂载后'))
// 更新
onBeforeUpdate(() => console.log('组件更新前'))
onUpdated(() => console.log('组件更新后'))
// 卸载
onBeforeUnmount(() => console.log('组件卸载前'))
onUnmounted(() => console.log('组件卸载后'))
// 错误处理
onErrorCaptured((err, instance, info) => {
console.error('捕获错误:', err)
return false // 阻止错误继续传播
})
// keep-alive 激活/停用
onActivated(() => console.log('组件被激活'))
onDeactivated(() => console.log('组件被停用'))
实战: 生命周期最佳实践
export default {
setup() {
// 所有响应式状态
const data = ref('')
const loading = ref(false)
const error = ref<string | null>(null)
// 获取数据
const fetchData = async () => {
loading.value = true
try {
const response = await fetch('/api/data')
data.value = await response.text()
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
// 组件挂载时获取数据
onMounted(() => {
fetchData()
})
// 组件卸载时清理
onUnmounted(() => {
// 取消请求、清除定时器等
data.value = null
})
// 返回模板需要的数据
return {
data,
loading,
error
}
}
}
4. 自定义 Composable (可复用逻辑)
创建 Composable 的规范
// ✅ composables/useCounter.ts
import { ref, computed } from 'vue'
export const useCounter = (initialValue: number = 0) => {
// 内部状态(私有)
const count = ref(initialValue)
// 暴露的计算属性
const double = computed(() => count.value * 2)
const isEven = computed(() => count.value % 2 === 0)
// 暴露的方法
const increment = () => count.value++
const decrement = () => count.value--
const reset = () => count.value = initialValue
// 返回暴露的 API
return {
count,
double,
isEven,
increment,
decrement,
reset
}
}
// 在组件中使用
<script setup lang="ts">
const { count, double, increment, reset } = useCounter(10)
</script>
<template>
<div>
<p>{{ count }} (double: {{ double }})</p>
<button @click="increment">+1</button>
<button @click="reset">Reset</button>
</div>
</template>
实战案例 1: 数据获取
// composables/useFetch.ts
export const useFetch = (url: string) => {
const data = ref(null)
const loading = ref(false)
const error = ref<string | null>(null)
const fetch = async () => {
loading.value = true
error.value = null
try {
const response = await $fetch(url)
data.value = response
} catch (e: any) {
error.value = e.message
} finally {
loading.value = false
}
}
// 组件挂载时自动获取
onMounted(() => fetch())
// 允许手动刷新
const refresh = () => fetch()
return {
data,
loading,
error,
refresh
}
}
// 使用
<script setup lang="ts">
const { data: users, loading, error, refresh } = useFetch('/api/users')
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error" class="error">{{ error }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="refresh">刷新</button>
</template>
实战案例 2: 表单处理
// composables/useForm.ts
export const useForm = (
initialValues: Record<string, any>,
onSubmit: (values: Record<string, any>) => Promise<void>
) => {
const form = reactive({ ...initialValues })
const errors = reactive<Record<string, string>>({})
const isSubmitting = ref(false)
const validate = () => {
// 简单验证示例
if (!form.email?.includes('@')) {
errors.email = '请输入有效的邮箱'
return false
}
return true
}
const handleSubmit = async () => {
if (!validate()) return
isSubmitting.value = true
try {
await onSubmit(form)
} catch (e: any) {
errors.submit = e.message
} finally {
isSubmitting.value = false
}
}
const reset = () => {
Object.assign(form, initialValues)
Object.keys(errors).forEach(key => delete errors[key])
}
return {
form,
errors,
isSubmitting,
handleSubmit,
reset
}
}
// 使用
<script setup lang="ts">
const { form, errors, isSubmitting, handleSubmit, reset } = useForm(
{ email: '', password: '' },
async (values) => {
await loginUser(values)
}
)
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="form.email" type="email" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
<input v-model="form.password" type="password" />
<button :disabled="isSubmitting" type="submit">
{{ isSubmitting ? '提交中...' : '提交' }}
</button>
<button type="button" @click="reset">重置</button>
</form>
</template>
实战案例 3: 事件处理
// composables/useEventListener.ts
export const useEventListener = (
target: any,
event: string,
callback: (event: Event) => void
) => {
onMounted(() => {
target.addEventListener(event, callback)
})
onUnmounted(() => {
target.removeEventListener(event, callback)
})
}
// 使用 - 窗口调整大小
<script setup lang="ts">
const windowWidth = ref(window.innerWidth)
useEventListener(window, 'resize', () => {
windowWidth.value = window.innerWidth
})
</script>
5. 高级用法
Composable 中使用其他 Composable
// ✅ 完全支持 composable 组合
// composables/useUser.ts
export const useUser = (userId: string) => {
const { data: user } = useFetch(`/api/users/${userId}`)
const updateUser = async (newData: any) => {
user.value = { ...user.value, ...newData }
await $fetch(`/api/users/${userId}`, {
method: 'PUT',
body: newData
})
}
return { user, updateUser }
}
// composables/useUserProfile.ts
export const useUserProfile = (userId: string) => {
// 使用其他 composable
const { user, updateUser } = useUser(userId)
const { data: posts } = useFetch(`/api/users/${userId}/posts`)
return { user, posts, updateUser }
}
响应式副作用
// watchEffect: 自动追踪依赖
import { watchEffect } from 'vue'
const userId = ref(1)
const user = ref(null)
watchEffect(async () => {
// userId 改变时自动重新执行
const response = await fetch(`/api/users/${userId.value}`)
user.value = await response.json()
})
// userId.value = 2 // 自动触发新的获取
6. 最佳实践
// ✅ Composable 最佳实践
// 1. 命名约定:use 前缀
export const useCounter = () => { /* ... */ }
export const useFetch = (url: string) => { /* ... */ }
// 2. 参数化 composables
export const useFetch = (url: string | Ref<string>) => {
// 支持响应式的 url
}
// 3. 返回响应式对象
export const useForm = () => {
return {
form, // 响应式状态
errors, // 响应式状态
submit, // 方法
reset // 方法
}
}
// 4. 处理加载和错误
const { data, loading, error, refresh } = useFetch('/api')
// 5. 避免副作用污染
export const useGoodComposable = () => {
// ✅ 好:副作用在组件生命周期内管理
onMounted(() => {
// 初始化
})
onUnmounted(() => {
// 清理
})
}
// ❌ 避免:直接执行副作用
export const useBadComposable = () => {
// ❌ 不好:每次导入都执行
console.log('这会每次都执行')
}
总结
| 特性 | 优势 |
|---|---|
| ref/reactive | 灵活的响应式管理 |
| computed | 派生状态和缓存 |
| watch | 灵活的侦听机制 |
| 生命周期 | 精确的时机控制 |
| Composable | 逻辑复用和组织 |
| TypeScript | 完整的类型支持 |