自定义 Hook 的设计与实现
Vue 3 的 Composition API 引入了函数式编程的灵活性,其中自定义 Hook 是其最强大的特性之一。自定义 Hook 允许开发者封装可复用的逻辑,提升代码的模块化和可维护性。本节将讲解自定义 Hook 的设计原则、实现步骤,并通过具体示例展示如何创建和使用它们,帮助你掌握这一高级功能。
什么是自定义 Hook?
自定义 Hook 是一个普通的 JavaScript 函数,通常以 use 开头,利用 Vue 的响应式 API(如 ref、reactive)封装特定功能。它与 React 的 Hook 概念类似,但专为 Vue 的 Composition API 设计。
- 作用:提取组件中的通用逻辑,使其可在多个组件间复用。
- 特点:
- 返回响应式数据和方法。
- 可组合多个 Hook。
- 支持 TypeScript 类型推导。
设计原则
设计自定义 Hook 时,应遵循以下原则:
- 单一职责:每个 Hook 专注于一个功能,避免过于复杂。
- 命名规范:以
use开头,如useCounter、useFetch,表明其为可复用逻辑。 - 参数灵活:支持配置参数,增强适用性。
- 响应性完整:确保返回的数据保持响应性。
- 生命周期管理:妥善处理副作用和清理。
实现步骤
- 定义功能:明确 Hook 的目标(如计数器、数据请求)。
- 使用响应式 API:根据需求选择
ref、reactive等。 - 封装逻辑:将数据和方法组织在一个函数中。
- 返回结果:以对象形式返回暴露的内容。
- 测试复用性:在多个组件中验证其功能。
示例 1:计数器 Hook
实现
创建一个简单的计数器 Hook,放在 src/composables/useCounter.js:
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
const count = ref(initialValue);
const increment = () => {
count.value++;
};
const decrement = () => {
count.value--;
};
const reset = () => {
count.value = initialValue;
};
return { count, increment, decrement, reset };
}
使用
在组件中使用:
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="increment">增加</button>
<button @click="decrement">减少</button>
<button @click="reset">重置</button>
</div>
</template>
<script>
import { useCounter } from '@/composables/useCounter';
export default {
setup() {
const { count, increment, decrement, reset } = useCounter(5); // 初始值为 5
return { count, increment, decrement, reset };
}
};
</script>
- 效果:计数器从 5 开始,点击按钮可增减或重置。
分析
- 参数化:支持自定义初始值。
- 简单清晰:专注于计数逻辑,返回必要的方法。
示例 2:数据请求 Hook
实现
创建一个用于数据请求的 Hook,放在 src/composables/useFetch.js:
import { ref, onMounted, onUnmounted } from 'vue';
export function useFetch(url) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);
const fetchData = async () => {
loading.value = true;
try {
const response = await fetch(url);
if (!response.ok) throw new Error('请求失败');
data.value = await response.json();
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
};
onMounted(fetchData); // 组件挂载时自动请求
// 可选:清理逻辑(例如取消请求)
onUnmounted(() => {
// 如果需要清理,可以在这里实现
});
return { data, loading, error, fetchData };
}
使用
在组件中使用:
<template>
<div>
<p v-if="loading">加载中...</p>
<p v-else-if="error">{{ error }}</p>
<ul v-else>
<li v-for="post in data" :key="post.id">{{ post.title }}</li>
</ul>
<button @click="fetchData">重新加载</button>
</div>
</template>
<script>
import { useFetch } from '@/composables/useFetch';
export default {
setup() {
const { data, loading, error, fetchData } = useFetch('https://jsonplaceholder.typicode.com/posts?_limit=5');
return { data, loading, error, fetchData };
}
};
</script>
- 效果:组件加载时自动获取数据,显示加载状态或错误,支持手动刷新。
分析
- 副作用管理:使用
onMounted自动请求,onUnmounted可清理。 - 状态完整:返回
data、loading和error,覆盖请求的常见状态。
示例 3:带参数和类型支持的 Hook
实现
创建一个鼠标位置跟踪 Hook,放在 src/composables/useMouse.js:
import { ref, onMounted, onUnmounted } from 'vue';
interface MousePosition {
x: number;
y: number;
}
export function useMouse(options: { throttle?: number } = {}) {
const { throttle = 100 } = options;
const position = ref<MousePosition>({ x: 0, y: 0 });
const updatePosition = (event: MouseEvent) => {
position.value.x = event.clientX;
position.value.y = event.clientY;
};
// 简单的节流实现
let timeout: number | null = null;
const throttledUpdate = (event: MouseEvent) => {
if (timeout) return;
timeout = setTimeout(() => {
updatePosition(event);
timeout = null;
}, throttle);
};
onMounted(() => {
window.addEventListener('mousemove', throttledUpdate);
});
onUnmounted(() => {
window.removeEventListener('mousemove', throttledUpdate);
if (timeout) clearTimeout(timeout);
});
return { position };
}
使用
<template>
<div>
<p>鼠标位置:X: {{ position.x }}, Y: {{ position.y }}</p>
</div>
</template>
<script>
import { useMouse } from '@/composables/useMouse';
export default {
setup() {
const { position } = useMouse({ throttle: 200 }); // 每 200ms 更新一次
return { position };
}
};
</script>
- 效果:鼠标移动时,位置更新受节流控制。
分析
- 参数化:支持
throttle配置。 - 类型支持:使用 TypeScript 定义接口。
- 清理:移除事件监听,避免内存泄漏。
设计注意事项
- 避免全局状态:
- 每个 Hook 实例应独立,避免共享状态。
- 错误示例:
const sharedCount = ref(0); // 全局共享,多个组件影响 export function useBadCounter() { return { count: sharedCount }; }
- 保持响应性:
- 返回值应为
ref或reactive对象。
- 返回值应为
- 模块化存放:
- 将 Hook 放入
composables/目录,按功能命名。
- 将 Hook 放入
- 文档化:
- 为复杂 Hook 添加注释或类型说明。
综合示例:组合多个 Hook
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="increment">增加</button>
<p v-if="loading">加载中...</p>
<ul v-else>
<li v-for="item in data" :key="item.id">{{ item.title }}</li>
</ul>
</div>
</template>
<script>
import { useCounter } from '@/composables/useCounter';
import { useFetch } from '@/composables/useFetch';
export default {
setup() {
const { count, increment } = useCounter();
const { data, loading } = useFetch('https://jsonplaceholder.typicode.com/todos?_limit=3');
return { count, increment, data, loading };
}
};
</script>
- 效果:显示计数器和从 API 获取的任务列表。
总结
自定义 Hook 是 Composition API 的精髓,通过封装逻辑提升了代码复用性和清晰度。本节通过计数器、数据请求和鼠标跟踪三个示例,展示了 Hook 的设计与实现过程。掌握这些技能后,你可以轻松创建自己的 Hook,应对复杂需求。下一节将探索高级用法,进一步扩展你的能力!
