Vue3中Hooks与普通函数的区别
在 Vue 3 中,组合式函数(Composition Functions,常被称为 Hooks) 和 普通函数 的区别主要体现在以下几个方面:
1. 依赖的上下文环境
-
组合式函数:
必须运行在 Vue 的组件上下文中(如setup()
或<script setup>
),因为它们依赖 Vue 的响应式系统(如ref
、reactive
)和生命周期钩子(如onMounted
、onUnmounted
)。javascript
// 组合式函数示例:useMouse import { ref, onMounted, onUnmounted } from 'vue';function useMouse() {const x = ref(0);const y = ref(0);function update(e) {x.value = e.pageX;y.value = e.pageY;}onMounted(() => window.addEventListener('mousemove', update));onUnmounted(() => window.removeEventListener('mousemove', update));return { x, y }; // 返回响应式数据 }
-
普通函数:
不依赖 Vue 的上下文,可以在任何地方调用(如工具函数、纯逻辑计算)。javascript
// 普通函数示例:求和 function add(a, b) {return a + b; }
2. 是否管理状态与副作用
-
组合式函数:
封装有状态的逻辑,可能包含响应式数据、副作用(如事件监听、API 请求)及其清理逻辑。javascript
// 管理副作用(生命周期钩子) onMounted(() => { /* ... */ }); onUnmounted(() => { /* ... */ });
-
普通函数:
通常是无状态的,不涉及响应式数据或副作用,仅处理输入并返回结果(纯函数)。javascript
// 无状态逻辑:格式化日期 function formatDate(date) {return new Date(date).toLocaleString(); }
3. 返回值类型
-
组合式函数:
通常返回响应式对象(如ref
、reactive
)或包含响应式数据的结构,供组件消费。javascript
const { x, y } = useMouse(); // x 和 y 是响应式的
-
普通函数:
返回静态数据或非响应式结果。javascript
const sum = add(1, 2); // 普通数字
4. 设计目的
-
组合式函数:
实现逻辑复用,将组件中复杂的响应式逻辑抽离为独立单元,类似 React Hooks。javascript
// 复用数据获取逻辑 function useFetch(url) {const data = ref(null);fetch(url).then(res => data.value = res.json());return { data }; }
-
普通函数:
封装通用工具逻辑(如数据处理、算法),不涉及组件状态或生命周期。
5. 命名约定
-
组合式函数:
社区约定以use
开头(如useFetch
、useStorage
),便于区分。 -
普通函数:
无特殊命名要求。
总结对比表
特性 | 组合式函数 (Hooks) | 普通函数 |
---|---|---|
依赖上下文 | 需在 Vue 组件上下文中调用 | 可在任何地方调用 |
响应式数据 | 操作 ref 、reactive 等响应式 API | 不涉及响应式系统 |
生命周期钩子 | 可调用 onMounted 、onUnmounted 等 | 无法使用生命周期钩子 |
副作用管理 | 封装副作用(如事件监听、API 请求) | 通常无副作用 |
返回值 | 返回响应式数据或结构 | 返回静态数据 |
目的 | 复用有状态逻辑 | 封装无状态工具逻辑 |
何时使用?
-
用组合式函数:需要封装涉及响应式数据、生命周期或副作用的逻辑(如鼠标跟踪、API 请求)。
-
用普通函数:处理纯数据转换、工具类操作(如日期格式化、数学计算)。
以下是一份使用 TypeScript 编写的 Vue 3 组合式函数(Hooks)的详细示例代码及解释,涵盖常见场景和高级用法:
示例 1:基础状态管理 Hook(useToggle
)
功能:封装布尔值的切换逻辑,支持类型安全的状态和方法。
typescript
复制
下载
// useToggle.ts import { ref, type Ref } from 'vue';interface UseToggleReturn {state: Ref<boolean>;toggle: () => void;set: (value: boolean) => void; }export function useToggle(initialValue: boolean = false): UseToggleReturn {const state = ref(initialValue);const toggle = () => {state.value = !state.value;};const set = (value: boolean) => {state.value = value;};return {state,toggle,set}; }
在组件中使用:
vue
复制
下载
<script setup lang="ts"> import { useToggle } from './useToggle';const { state: isDarkMode, toggle } = useToggle(true); </script><template><button @click="toggle">{{ isDarkMode ? '☀️ Light' : '🌙 Dark' }} Mode</button> </template>
关键点:
-
类型定义:使用
interface
明确定义返回值类型UseToggleReturn
。 -
泛型参数:
ref<boolean>
确保状态类型安全。 -
代码提示:组件中使用时会自动提示
toggle
和set
方法。
示例 2:带泛型的异步请求 Hook(useFetch
)
功能:封装数据请求逻辑,支持泛型类型推断。
typescript
复制
下载
// useFetch.ts import { ref, type Ref } from 'vue';interface UseFetchReturn<T> {data: Ref<T | null>;error: Ref<string | null>;isLoading: Ref<boolean>;retry: () => Promise<void>; }export function useFetch<T>(url: string): UseFetchReturn<T> {const data = ref<T | null>(null) as Ref<T | null>;const error = ref<string | null>(null);const isLoading = ref(false);const fetchData = async (): Promise<void> => {isLoading.value = true;try {const response = await fetch(url);if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`);data.value = await response.json() as T;error.value = null;} catch (err: unknown) {error.value = err instanceof Error ? err.message : 'Unknown error';} finally {isLoading.value = false;}};fetchData();return {data,error,isLoading,retry: fetchData}; }
在组件中使用:
vue
复制
下载
<script setup lang="ts"> import { useFetch } from './useFetch';interface Post {id: number;title: string;content: string; }const { data: posts, isLoading } = useFetch<Post[]>('https://api.example.com/posts'); </script><template><div v-if="isLoading">Loading...</div><ul v-else><li v-for="post in posts" :key="post.id">{{ post.title }}</li></ul> </template>
关键点:
-
泛型参数:
useFetch<T>
允许在调用时指定数据类型(如Post[]
)。 -
类型断言:
data.value = await response.json() as T
确保类型正确。 -
错误处理:使用
instanceof Error
安全地获取错误信息。
示例 3:联合类型和副作用管理的 Hook(useStorage
)
功能:封装 localStorage
操作,支持联合类型和自动同步。
typescript
复制
下载
// useStorage.ts import { ref, watchEffect, type Ref } from 'vue';type StorageValue<T> = T | null;export function useStorage<T>(key: string,defaultValue: T ): {storedValue: Ref<StorageValue<T>>;setValue: (value: T) => void;removeValue: () => void; } {const storedValue = ref<StorageValue<T>>(defaultValue);// 初始化时读取 localStoragetry {const item = localStorage.getItem(key);storedValue.value = item ? (JSON.parse(item) as T) : defaultValue;} catch (error) {console.error('Error reading localStorage:', error);}// 监听变化并同步到 localStoragewatchEffect(() => {try {if (storedValue.value === null) {localStorage.removeItem(key);} else {localStorage.setItem(key, JSON.stringify(storedValue.value));}} catch (error) {console.error('Error writing to localStorage:', error);}});const setValue = (value: T) => {storedValue.value = value;};const removeValue = () => {storedValue.value = null;};return {storedValue,setValue,removeValue}; }
在组件中使用:
vue
复制
下载
<script setup lang="ts"> import { useStorage } from './useStorage';const { storedValue: theme, setValue: setTheme } = useStorage<string>('app-theme', 'light'); </script><template><select :value="theme" @change="setTheme(($event.target as HTMLSelectElement).value)"><option value="light">Light</option><option value="dark">Dark</option></select> </template>
关键点:
-
联合类型:
StorageValue<T>
表示值可以是T
或null
。 -
类型安全操作:
setValue
强制接受T
类型参数。 -
错误边界:使用
try/catch
处理localStorage
可能抛出的异常。
示例 4:复杂表单验证 Hook(useFormValidation
)
功能:类型安全的表单验证,支持动态规则和错误收集。
typescript
复制
下载
// useFormValidation.ts import { ref, computed, type Ref } from 'vue';interface ValidationRule<T> {validator: (value: T) => boolean;message: string; }interface UseFormValidationReturn<T extends Record<string, any>> {errors: Ref<Record<keyof T, string[]>>;validateField: <K extends keyof T>(field: K, value: T[K]) => void;isValid: Ref<boolean>; }export function useFormValidation<T extends Record<string, any>>(rules: Record<keyof T, ValidationRule<T[keyof T]>[]> ): UseFormValidationReturn<T> {const errors = ref<Record<keyof T, string[]>>(Object.keys(rules).reduce((acc, key) => {acc[key as keyof T] = [];return acc;}, {} as Record<keyof T, string[]>));const validateField = <K extends keyof T>(field: K, value: T[K]): void => {errors.value[field] = [];rules[field].forEach((rule) => {if (!rule.validator(value)) {errors.value[field].push(rule.message);}});};const isValid = computed(() => {return Object.values(errors.value).every((e) => e.length === 0);});return {errors,validateField,isValid}; }
在组件中使用:
vue
复制
下载
<script setup lang="ts"> import { ref } from 'vue'; import { useFormValidation } from './useFormValidation';interface FormValues {email: string;password: string; }const form = ref<FormValues>({email: '',password: '' });const { errors, validateField, isValid } = useFormValidation<FormValues>({email: [{validator: (v) => v.includes('@'),message: 'Email must contain @'}],password: [{validator: (v) => v.length >= 6,message: 'Password must be at least 6 characters'}] }); </script><template><input v-model="form.email" @blur="validateField('email', form.email)"/><div v-if="errors.email.length">{{ errors.email.join(', ') }}</div><inputv-model="form.password"type="password"@blur="validateField('password', form.password)"/><div v-if="errors.password.length">{{ errors.password.join(', ') }}</div><button :disabled="!isValid">Submit</button> </template>
关键点:
-
泛型约束:
T extends Record<string, any>
确保传入的表单结构合法。 -
动态字段类型:
validateField<K extends keyof T>
自动推断字段类型。 -
类型安全规则:
ValidationRule<T[keyof T]>[]
确保验证规则与字段类型匹配。
高级场景:组合多个 Hooks(useUserDashboard
)
功能:组合数据请求、状态管理和事件监听。
typescript
复制
下载
// useUserDashboard.ts import { computed, type Ref } from 'vue'; import { useFetch } from './useFetch'; import { useEventListener } from './useEventListener';interface User {id: number;name: string;email: string; }interface UserDashboard {user: Ref<User | null>;isLoading: Ref<boolean>;error: Ref<string | null>;refresh: () => void; }export function useUserDashboard(userId: number): UserDashboard {const { data: user, isLoading, error, retry } = useFetch<User>(`/api/users/${userId}`);// 监听窗口聚焦时刷新数据useEventListener(window, 'focus', retry);return {user,isLoading,error,refresh: retry}; }
在组件中使用:
vue
复制
下载
<script setup lang="ts"> import { useUserDashboard } from './useUserDashboard';const { user, isLoading } = useUserDashboard(123); </script><template><div v-if="isLoading">Loading user...</div><div v-else><h1>{{ user?.name }}</h1><p>{{ user?.email }}</p></div> </template>
关键点:
-
组合复用:通过
useFetch
和useEventListener
构建复杂逻辑。 -
自动刷新:窗口聚焦时自动重新请求数据。
-
类型推断:
useFetch<User>
确保返回的用户数据符合User
接口。
TypeScript 的优势总结
-
类型安全
通过接口 (interface
) 和泛型 (Generic
) 明确数据类型,减少运行时错误。 -
代码提示
自动补全返回值和方法,提升开发效率。 -
复杂类型处理
使用keyof
、extends
等高级类型操作,处理动态数据结构。 -
错误预防
在编译阶段捕获类型不匹配问题(如尝试将字符串赋值给数字类型)。
最佳实践建议
-
始终定义接口
为复杂数据结构和函数返回值编写interface
/type
。 -
合理使用泛型
在需要动态类型的场景(如useFetch<T>
)中使用泛型。 -
类型断言谨慎使用
仅在明确知道类型时使用as
(如 API 响应数据)。 -
利用工具类型
使用Partial<T>
、Pick<T, K>
等内置工具类型简化代码。 -
严格的
tsconfig.json
启用strict: true
等选项以最大化类型检查效益。
通过这些示例,你可以看到 TypeScript 如何显著提升 Vue 3 组合式函数的可靠性和开发体验。它不仅帮助捕获潜在错误,还能通过清晰的类型定义让代码更易于理解和维护。