Vue Composables 原理和使用
Vue 3 引入的 Composition API 彻底改变了我们编写 Vue 组件的方式,而 Composables 则是基于此 API 构建的可复用逻辑单元。它们让我们能够提取和重用有状态逻辑,同时保持组件间的状态隔离,大大提升了代码的模块化和可维护性。
1. Composition API:Vue 3 的响应式核心
Composition API 是 Vue 3 引入的一套新 API,用于组织和复用组件逻辑。它通过 setup
函数来定义组件的逻辑,使得我们可以更灵活地使用 JavaScript 特性来组织代码。
1.1 核心概念
setup
函数: 是 Composition API 的入口,在组件实例创建之前执行。它返回一个对象,该对象中的属性和方法将被暴露给模板使用。- 响应式 API: 如
ref
、reactive
、computed
和watch
,用于创建和管理响应式状态。 - 生命周期钩子: 如
onMounted
、onUpdated
和onUnmounted
,用于处理组件的生命周期事件。
1.2 示例:基础用法
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
}
</script>
在这个示例中,setup
函数返回的 count
和 increment
被暴露给模板使用。我们使用 ref
创建了一个响应式变量 count
,并定义了一个 increment
方法来增加计数。
2. Composition:代码组织的理念
Composition 是一种将逻辑和功能分离成独立的、可重用部分的概念。在 Vue 3 中,Composition API 实现了这一概念,使得我们可以将组件逻辑组织成独立的模块,称为 Composables。
2.1 Composition 的优势
- 模块化: 将逻辑分离成独立模块,增强代码的可读性和可维护性。
- 复用性: 独立的逻辑模块可以在多个组件中复用,减少代码重复。
- 灵活性: 更灵活地组合和管理逻辑,使代码更易于扩展和修改。
2.2 示例:逻辑分离
// useCounter.js
import { ref } from 'vue'
export function useCounter() {
const count = ref(0)
const increment = () => {
count.value++
}
return {
count,
increment
}
}
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
import { useCounter } from './useCounter'
export default {
setup() {
const { count, increment } = useCounter()
return {
count,
increment
}
}
}
</script>
在这个示例中,我们将计数逻辑提取到 useCounter
函数中,使其成为一个独立的模块。然后在组件中使用这个模块,实现逻辑的复用。
3. Composables:可复用的逻辑函数
Composables 是使用 Composition API 创建的函数,用于封装和复用逻辑。它们通常以 use
开头,表示这是一个可复用的逻辑模块。例如,useCounter
是一个计数逻辑的 Composable。
3.1 Composables 的优势
- 逻辑复用: 将逻辑封装在 Composables 中,可以在不同的组件中复用,减少代码重复。
- 易于测试: 独立的逻辑模块更易于测试和调试。
- 增强可维护性: 将复杂的逻辑分解成简单的、可复用的函数,提高代码的可维护性。
3.2 示例:数据获取 Composable
// useFetch.js
import { ref, onMounted } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
const fetchData = async () => {
try {
const response = await fetch(url)
data.value = await response.json()
} catch (err) {
error.value = err
}
}
onMounted(fetchData)
return {
data,
error,
fetchData
}
}
<template>
<div>
<div v-if="error">{{ error.message }}</div>
<div v-else-if="data">{{ data }}</div>
<div v-else>Loading...</div>
</div>
</template>
<script>
import { useFetch } from './useFetch'
export default {
setup() {
const { data, error } = useFetch('https://api.example.com/data')
return {
data,
error
}
}
}
</script>
在这个示例中,我们创建了一个 useFetch
Composable,用于封装数据获取的逻辑。然后在组件中使用这个 Composable,实现数据获取功能的复用。
4. Composables 与 Mixins 的对比
在 Vue 3 引入 Composables 之前,开发者通常使用 Mixins 来实现代码复用。虽然 Mixins 有一定的作用,但它们存在几个明显的问题,而 Composables 则提供了更好的解决方案。
4.1 Mixins 的问题
- 命名冲突: 多个 Mixins 可能会使用相同的属性名,导致意外覆盖。
- 隐式依赖: 很难清楚地知道 Mixin 使用了哪些属性和方法。
- 关系不明确: 当多个 Mixins 交互时,逻辑关系变得复杂难懂。
4.2 Composables 的优势
- 显式依赖: 通过导入和调用函数,依赖关系更加明确。
- 命名空间: 可以重命名解构的变量,避免命名冲突。
- 类型支持: 更好的 TypeScript 支持,提供完整的类型推断。
4.3 示例:表单验证对比
Mixin 方式:
// formValidation.js
export const formValidationMixin = {
data() {
return {
formData: {
username: '',
password: '',
},
formErrors: {
username: '',
password: '',
},
};
},
methods: {
validateForm() {
this.formErrors = {};
if (!this.formData.username.trim()) {
this.formErrors.username = 'Username is required.';
}
if (!this.formData.password.trim()) {
this.formErrors.password = 'Password is required.';
}
return Object.keys(this.formErrors).length === 0;
},
},
};
Composable 方式:
// formValidation.js
import { reactive } from 'vue';
export function useFormValidation() {
const state = reactive({
formData: {
username: '',
password: '',
},
formErrors: {
username: '',
password: '',
},
});
function validateForm() {
state.formErrors = {};
if (!state.formData.username.trim()) {
state.formErrors.username = 'Username is required.';
}
if (!state.formData.password.trim()) {
state.formErrors.password = 'Password is required.';
}
return Object.keys(state.formErrors).length === 0;
}
return {
state,
validateForm,
};
}
Composable 方式提供了更清晰的代码结构和更好的类型支持。
5. 高级 Composable 模式
5.1 参数化 Composables
Composables 可以接受参数,使其更加灵活和可配置:
import { ref, watchEffect, toValue } from 'vue'
export function useStringFormatter(data, separator) {
const formatted = ref('')
watchEffect(() => {
formatted.value = toValue(data).replace(/\s+/g, toValue(separator))
})
return {
formatted
}
}
在这个示例中,useStringFormatter
Composable 接受 data
和 separator
参数,并使用 watchEffect
来响应参数的变化。
5.2 共享状态 Composables
有时我们需要在多个组件之间共享状态,可以使用闭包和 EffectScope
来实现:
import { effectScope, ref, onScopeDispose } from 'vue'
export function createSharedComposable(composable) {
let subscribers = 0
let state
let scope
const dispose = () => {
subscribers--
if (scope && subscribers <= 0) {
scope.stop()
state = undefined
scope = undefined
}
}
return (...args) => {
subscribers++
if (!scope) {
scope = effectScope(true)
state = scope.run(() => composable(...args))
}
onScopeDispose(dispose)
return state
}
}
// 使用示例
function useCounter() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
}
const useSharedCounter = createSharedComposable(useCounter)
这个 createSharedComposable
函数可以将普通的 Composable 转换为共享的 Composable,在多个组件之间共享同一份状态和副作用。
5.3 生命周期感知的 Composables
有时我们需要在 Composables 中感知组件的生命周期,可以创建专门的生命周期 Composable:
// composables/useLifecycle.ts
import { ref, onMounted, onUpdated, onUnmounted } from 'vue'
export type LifecycleState =
'beforeCreate' | 'created' | 'beforeMount' |
'mounted' | 'beforeUpdate' | 'updated' |
'beforeUnmount' | 'unmounted'
export function useLifecycle() {
const lifecycle = ref<LifecycleState>('beforeCreate')
// 生命周期映射表
const hooks = {
created: () => lifecycle.value = 'created',
beforeMount: () => lifecycle.value = 'beforeMount',
mounted: () => lifecycle.value = 'mounted',
beforeUpdate: () => lifecycle.value = 'beforeUpdate',
updated: () => lifecycle.value = 'updated',
beforeUnmount: () => lifecycle.value = 'beforeUnmount',
unmounted: () => lifecycle.value = 'unmounted'
}
// 注册生命周期钩子
onMounted(hooks.mounted)
onUpdated(hooks.updated)
onUnmounted(hooks.unmounted)
// 返回带重置功能的对象
return {
lifecycle,
reset: () => lifecycle.value = 'beforeCreate'
}
}
这个 useLifecycle
Composable 可以追踪组件的生命周期状态,在调试和特定生命周期操作时非常有用。
6. TypeScript 集成
TypeScript 为 Composables 提供了强大的类型支持,增强了代码的可靠性和开发体验。
6.1 基础类型定义
// types.ts
export interface FetchResult<T> {
data: Ref<T | null>
error: Ref<Error | null>
isLoading: Ref<boolean>
}
// useFetch.ts
import { ref } from 'vue'
export function useFetch<T>(url: string) {
const data = ref<T | null>(null)
const error = ref<Error | null>(null)
const isLoading = ref(false)
const execute = async () => {
isLoading.value = true
try {
const response = await fetch(url)
data.value = await response.json() as T
} catch (err) {
error.value = err as Error
} finally {
isLoading.value = false
}
}
return { data, error, isLoading, execute }
}
6.2 高级类型技巧
// 泛型参数约束
export function useFetch<T extends object>(url: string) {
// 约束泛型必须是对象类型
}
// 错误类型细化
class ApiError extends Error {
statusCode: number
constructor(message: string, statusCode: number) {
super(message)
this.statusCode = statusCode
}
}
// 配置对象模式
interface FetchOptions {
immediate?: boolean
timeout?: number
}
function useFetch<T>(url: string, options?: FetchOptions) {
// 实现配置处理
}
// 请求取消支持
const controller = new AbortController()
fetch(url, {
signal: controller.signal
})
// 组件卸载时取消请求
onUnmounted(() => controller.abort())
TypeScript 集成提供了编译时错误预防、IDE 智能提示、接口变更快速反馈和代码自文档化等优势。
7. VueUse:实用的 Composable 库
VueUse 是一个基于 Vue 3 Composition API 的实用工具库,提供了 200+ 可复用的组合式函数,覆盖状态管理、浏览器 API、动画、响应式布局等常见开发场景。
7.1 常用功能示例
useFetch: 简化 HTTP 请求处理
<script setup lang="ts">
import { ref } from 'vue'
import { useFetch } from '@vueuse/core'
const { data, error, isFetching } = useFetch('https://api.example.com/data')
const fetchData = async () => {
if (isFetching.value) return
await data.value // 等待数据加载完成
console.log(data.value)
}
if (error.value) {
console.error('Fetch Error:', error.value)
}
</script>
useStorage: 本地存储管理
<script setup lang="ts">
import { useStorage } from '@vueuse/core'
const name = useStorage('user-name', '')
</script>
useMouse: 鼠标位置追踪
<script setup lang="ts">
import { useMouse } from '@vueuse/core'
const { x, y } = useMouse()
</script>
useDark: 暗黑模式切换
<script setup lang="ts">
import { useDark, useToggle } from '@vueuse/core'
const isDark = useDark()
const toggleDark = useToggle(isDark)
</script>
7.2 VueUse 的优势
- 减少重复代码: 200+ 函数覆盖 90% 常见场景
- 提升开发效率: 从编写逻辑到测试的全流程优化
- 增强代码质量: 社区维护的稳定实现
- 拥抱 Vue3 生态: 与 Composition API 深度集成
8. 最佳实践与注意事项
8.1 命名约定
Composable 函数通常以 use
开头,表示这是一个可复用的逻辑模块。这种命名约定有助于区分普通函数和 Composables。
8.2 响应式状态管理
- 优先使用
ref
而不是reactive
,因为ref
保持响应性并支持解构 - 对于复杂对象,可以使用
reactive
但要注意失去响应性的风险 - 使用
computed
来处理派生状态
8.3 副作用管理
- 使用
watch
和watchEffect
来响应状态变化 - 确保清理副作用,防止内存泄漏
- 使用
onUnmounted
或onScopeDispose
来清理资源
8.4 性能优化
- 按需引入 Composables,避免不必要的代码加载
- 使用防抖和节流优化频繁操作
- 考虑使用共享 Composables 来避免重复计算
8.5 测试策略
- 独立测试 Composables,确保逻辑正确性
- 使用 Vue Test Utils 或专门的测试库
- 模拟依赖项,确保测试的隔离性
9. 实战案例:完整的待办事项应用
让我们通过一个完整的待办事项应用来展示 Composables 的实际应用:
// composables/useTodoList.ts
import { ref, computed } from 'vue'
export interface Todo {
id: number
text: string
completed: boolean
createdAt: Date
}
export function useTodoList() {
const todos = ref<Todo[]>([])
const filter = ref<'all' | 'active' | 'completed'>('all')
const addTodo = (text: string) => {
if (!text.trim()) return
todos.value.push({
id: Date.now(),
text: text.trim(),
completed: false,
createdAt: new Date()
})
}
const removeTodo = (id: number) => {
todos.value = todos.value.filter(todo => todo.id !== id)
}
const toggleTodo = (id: number) => {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
const clearCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed)
}
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
const stats = computed(() => {
const total = todos.value.length
const completed = todos.value.filter(todo => todo.completed).length
const active = total - completed
return { total, completed, active }
})
return {
todos: filteredTodos,
filter,
stats,
addTodo,
removeTodo,
toggleTodo,
clearCompleted
}
}
<template>
<div class="todo-app">
<h1>待办事项</h1>
<div class="add-form">
<input
v-model="newTodo"
@keyup.enter="addTodo(newTodo)"
placeholder="添加新任务..."
class="todo-input"
>
<button @click="addTodo(newTodo)" class="add-btn">添加</button>
</div>
<div class="filters">
<button
:class="{ active: filter === 'all' }"
@click="filter = 'all'"
>
全部 ({{ stats.total }})
</button>
<button
:class="{ active: filter === 'active' }"
@click="filter = 'active'"
>
待办 ({{ stats.active }})
</button>
<button
:class="{ active: filter === 'completed' }"
@click="filter = 'completed'"
>
已完成 ({{ stats.completed }})
</button>
</div>
<ul class="todo-list">
<li v-for="todo in todos" :key="todo.id" class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
class="todo-checkbox"
>
<span :class="{ completed: todo.completed }" class="todo-text">
{{ todo.text }}
</span>
<button @click="removeTodo(todo.id)" class="delete-btn">删除</button>
</li>
</ul>
<div v-if="stats.completed > 0" class="clear-container">
<button @click="clearCompleted" class="clear-btn">
清除已完成 ({{ stats.completed }})
</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useTodoList } from './composables/useTodoList'
const newTodo = ref('')
const { todos, filter, stats, addTodo, removeTodo, toggleTodo, clearCompleted } = useTodoList()
const addTodoAndClear = (text: string) => {
addTodo(text)
newTodo.value = ''
}
</script>
这个示例展示了如何使用 Composable 来管理复杂的应用状态,包括添加、删除、切换和过滤待办事项等功能。
10. 总结
Vue Composables 是 Vue 3 中一个强大的特性,它彻底改变了我们组织和复用代码的方式。通过本文的探讨,我们可以总结出以下几点关键见解:
10.1 核心价值
- 逻辑复用: Composables 允许我们将有状态逻辑提取到可重用的函数中,减少代码重复和提高一致性。
- 代码组织: 通过功能而不是选项类型来组织代码,使相关逻辑保持在一起,提高可读性和可维护性。
- 类型安全: 与 TypeScript 的深度集成提供了更好的开发体验和更可靠的代码。
10.2 适用场景
- 复杂状态逻辑: 当组件包含复杂的状态管理逻辑时,使用 Composables 可以更好地组织代码。
- 跨组件复用: 当多个组件需要共享相同或类似的逻辑时,Composables 提供了一种干净的解决方案。
- 第三方集成: 当需要与非 Vue 库或 API 集成时,Composables 可以封装复杂性。
10.3 最佳实践
- 从简单的 Composables 开始,逐步扩展到更复杂的场景
- 合理使用响应式 API,理解
ref
和reactive
的区别和适用场景 - 注意副作用管理,确保及时清理资源防止内存泄漏
- 充分利用 TypeScript 提供类型安全和更好的开发体验
- 考虑使用 VueUse 等现有库,避免重复造轮子