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 组合式函数的可靠性和开发体验。它不仅帮助捕获潜在错误,还能通过清晰的类型定义让代码更易于理解和维护。
