vue3——自定义Hook
一、自定义Hook基础概念1.1 什么是自定义Hook?1.2 第一个自定义Hook:useCounter二、自定义Hook进阶应用2.1 带选项的自定义Hook:useLocalStorage2.2 异步数据获取Hook:useFetch三、组合多个Hook的高级应用3.1 组合Hook示例:useUserData四、自定义Hook最佳实践4.1 命名规范4.2 代码组织4.3 状态管理4.4
·
Vue3自定义Hook完全指南
一、自定义Hook基础概念
1.1 什么是自定义Hook?
自定义Hook是Vue3组合式API中一种封装和复用组件逻辑的方式,本质上是一个以use为前缀命名的函数,它可以调用其他的Hook,并将组件逻辑提取到可重用的函数中。
核心特点:
- 命名必须以
use开头(如useCounter、useFetch) - 可以调用其他Hook(如
ref、reactive、onMounted等) - 可以返回任意类型的数据(基本类型、对象、函数等)
- 每次调用都会创建独立的状态,互不干扰
为什么需要自定义Hook:
- 逻辑复用:将组件中可复用的逻辑提取为Hook
- 代码组织:使组件代码更加清晰,按功能划分
- 关注点分离:将数据获取、状态管理等不同关注点分离
- 测试友好:独立的Hook更容易进行单元测试
1.2 第一个自定义Hook:useCounter
// hooks/useCounter.js
import { ref, computed } from 'vue';
// 定义自定义Hook,接收初始值和步长作为参数
export function useCounter(initialValue = 0, step = 1) {
// 定义响应式状态
const count = ref(initialValue);
// 定义修改状态的方法
const increment = () => {
count.value += step;
};
const decrement = () => {
count.value -= step;
};
const reset = () => {
count.value = initialValue;
};
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 返回需要暴露的状态和方法
return {
count,
doubleCount,
increment,
decrement,
reset
};
}
在组件中使用:
<template>
<div class="counter-demo">
<h2>计数器示例</h2>
<p>当前计数: {{ count }}</p>
<p>双倍计数: {{ doubleCount }}</p>
<div class="button-group">
<button @click="decrement">-</button>
<button @click="reset">重置</button>
<button @click="increment">+</button>
</div>
</div>
</template>
<script setup>
// 导入自定义Hook
import { useCounter } from './hooks/useCounter';
// 调用Hook,获取状态和方法
const { count, doubleCount, increment, decrement, reset } = useCounter(0, 1);
</script>
<style>
.button-group {
display: flex;
gap: 10px;
margin-top: 10px;
}
button {
padding: 5px 15px;
cursor: pointer;
}
</style>
运行结果:
- 页面显示计数器初始值为0,双倍计数为0
- 点击"+"按钮,计数增加1,双倍计数同步增加2
- 点击"-"按钮,计数减少1,双倍计数同步减少2
- 点击"重置"按钮,计数恢复为初始值0
- 所有状态变化都是响应式的,UI会实时更新
代码解析:
useCounter函数接收initialValue和step两个参数,分别设置初始值和步长- 内部使用
ref创建响应式状态count - 定义了
increment、decrement和reset三个方法来修改状态 - 使用
computed创建计算属性doubleCount - 返回一个对象,包含需要暴露给组件的状态和方法
- 组件中通过解构赋值获取Hook提供的状态和方法,并在模板中使用
二、自定义Hook进阶应用
2.1 带选项的自定义Hook:useLocalStorage
// hooks/useLocalStorage.js
import { ref, watch, onMounted } from 'vue';
// 自定义Hook:用于在localStorage中存储和读取数据
export function useLocalStorage(key, options = {}) {
// 从选项中获取默认值和是否自动保存
const { defaultValue = null, autoSave = true } = options;
// 创建响应式状态
const data = ref(defaultValue);
// 从localStorage加载数据
const load = () => {
try {
const storedValue = localStorage.getItem(key);
if (storedValue !== null) {
// 尝试解析JSON,如果失败则直接使用原始值
data.value = JSON.parse(storedValue);
} else {
data.value = defaultValue;
// 如果没有存储值且提供了默认值,则保存默认值
if (defaultValue !== null) {
save();
}
}
} catch (error) {
console.error('Failed to load data from localStorage:', error);
data.value = defaultValue;
}
};
// 保存数据到localStorage
const save = () => {
try {
localStorage.setItem(key, JSON.stringify(data.value));
} catch (error) {
console.error('Failed to save data to localStorage:', error);
}
};
// 清除localStorage中的数据
const remove = () => {
try {
localStorage.removeItem(key);
data.value = defaultValue;
} catch (error) {
console.error('Failed to remove data from localStorage:', error);
}
};
// 组件挂载时加载数据
onMounted(() => {
load();
});
// 如果启用了自动保存,则监听数据变化并自动保存
if (autoSave) {
watch(data, () => {
save();
}, { deep: true }); // deep: true 用于监听对象内部变化
}
// 返回状态和方法
return {
data,
load,
save,
remove
};
}
在组件中使用:
<template>
<div class="local-storage-demo">
<h2>LocalStorage示例</h2>
<div class="input-group">
<label>用户名:</label>
<input v-model="username.data" placeholder="请输入用户名">
</div>
<div class="input-group">
<label>年龄:</label>
<input v-model.number="userInfo.data.age" type="number" placeholder="请输入年龄">
</div>
<div class="button-group">
<button @click="userInfo.save">手动保存</button>
<button @click="userInfo.remove">清除数据</button>
<button @click="userInfo.load">重新加载</button>
</div>
<div class="status">
<p>当前存储的数据: {{ userInfo.data }}</p>
</div>
</div>
</template>
<script setup>
import { useLocalStorage } from './hooks/useLocalStorage';
// 使用自定义Hook存储用户名(自动保存)
const username = useLocalStorage('username', {
defaultValue: 'Guest',
autoSave: true
});
// 使用自定义Hook存储用户信息(手动保存)
const userInfo = useLocalStorage('userInfo', {
defaultValue: { name: '', age: 0 },
autoSave: false // 禁用自动保存,需要手动调用save方法
});
</script>
<style>
.input-group {
margin: 10px 0;
}
input {
margin-left: 10px;
padding: 5px;
}
.button-group {
margin: 15px 0;
display: flex;
gap: 10px;
}
button {
padding: 5px 10px;
cursor: pointer;
}
.status {
margin-top: 15px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
</style>
运行结果:
- 页面加载时,会从localStorage加载数据,如果没有数据则使用默认值
- 用户名输入框:输入内容时会自动保存到localStorage(因为autoSave: true)
- 用户信息区域:
- 输入年龄后不会立即保存(因为autoSave: false)
- 点击"手动保存"按钮后才会保存到localStorage
- 点击"清除数据"按钮会删除localStorage中的数据并重置为默认值
- 点击"重新加载"按钮会从localStorage重新加载数据
- 刷新页面后,数据会从localStorage中恢复
代码解析:
useLocalStorage接收两个参数:存储的键名和选项对象- 选项对象支持
defaultValue(默认值)和autoSave(是否自动保存) - 提供了
load、save、remove三个方法手动操作数据 - 使用
onMounted在组件挂载时自动加载数据 - 使用
watch监听数据变化,当autoSave为true时自动保存 - 通过
{ deep: true }选项支持对象类型数据的深度监听
2.2 异步数据获取Hook:useFetch
// hooks/useFetch.js
import { ref, onMounted, watch, computed } from 'vue';
export function useFetch(url, options = {}) {
// 定义响应式状态
const data = ref(null);
const error = ref(null);
const loading = ref(false);
// 默认请求选项
const defaultOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
immediate: true, // 是否在挂载后立即请求
...options
};
// 发送请求的函数
const fetchData = async (customUrl = null, customOptions = {}) => {
// 合并URL和选项
const requestUrl = customUrl || url;
const requestOptions = { ...defaultOptions, ...customOptions };
// 如果没有URL,抛出错误
if (!requestUrl) {
error.value = new Error('URL is required for fetch');
return;
}
// 重置状态
data.value = null;
error.value = null;
loading.value = true;
try {
// 发送请求
const response = await fetch(requestUrl, requestOptions);
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// 解析响应数据(支持JSON和文本)
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
data.value = await response.json();
} else {
data.value = await response.text();
}
} catch (err) {
// 捕获错误
error.value = err;
console.error('Fetch error:', err);
} finally {
// 请求完成,更新loading状态
loading.value = false;
}
};
// 如果设置了immediate,在组件挂载时自动请求
if (defaultOptions.immediate) {
onMounted(() => {
fetchData();
});
}
// 监听URL变化,如果URL是ref则自动重新请求
if (url && typeof url !== 'string') {
watch(url, (newUrl) => {
if (newUrl) {
fetchData(newUrl);
}
});
}
// 计算属性:是否处于空闲状态(既不加载也无错误)
const isIdle = computed(() => !loading.value && !error.value && data.value === null);
// 计算属性:是否请求成功
const isSuccess = computed(() => !loading.value && !error.value && data.value !== null);
return {
data,
error,
loading,
isIdle,
isSuccess,
fetchData
};
}
在组件中使用:
<template>
<div class="fetch-demo">
<h2>数据获取示例</h2>
<div class="controls">
<input v-model="postId" type="number" min="1" max="100" placeholder="输入帖子ID">
<button @click="fetchPost">获取帖子</button>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="status loading">
加载中...
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="status error">
错误: {{ error.message }}
</div>
<!-- 空闲状态 -->
<div v-else-if="isIdle" class="status idle">
请输入帖子ID并点击"获取帖子"按钮
</div>
<!-- 成功状态 -->
<div v-else-if="isSuccess" class="post">
<h3>{{ data.title }}</h3>
<div class="post-meta">
<span>作者ID: {{ data.userId }}</span>
<span>帖子ID: {{ data.id }}</span>
</div>
<div class="post-body">
{{ data.body }}
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useFetch } from './hooks/useFetch';
// 帖子ID
const postId = ref(1);
// 创建自定义Hook实例,不立即请求(immediate: false)
const {
data,
error,
loading,
isIdle,
isSuccess,
fetchData: fetchPost
} = useFetch(null, {
immediate: false
});
// 自定义获取帖子的方法
const fetchData = () => {
// 调用Hook提供的fetchData方法,传入动态URL
fetchPost(`https://jsonplaceholder.typicode.com/posts/${postId.value}`);
};
</script>
<style>
.controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
}
input {
padding: 5px;
width: 100px;
}
button {
padding: 5px 15px;
cursor: pointer;
}
.status {
padding: 15px;
border-radius: 4px;
margin: 10px 0;
}
.loading {
background-color: #e3f2fd;
color: #1976d2;
}
.error {
background-color: #ffebee;
color: #d32f2f;
}
.idle {
background-color: #fff8e1;
color: #ff8f00;
}
.post {
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 15px;
margin-top: 10px;
}
.post-meta {
color: #666;
font-size: 0.9em;
margin: 5px 0 15px;
display: flex;
gap: 15px;
}
.post-body {
line-height: 1.6;
}
</style>
运行结果:
- 初始状态显示"请输入帖子ID并点击’获取帖子’按钮"
- 输入1-100之间的数字并点击"获取帖子"按钮:
- 显示"加载中…"状态
- 加载完成后显示帖子标题、作者ID、帖子ID和内容
- 如果输入无效ID或网络错误:
- 显示错误信息,如"HTTP error! status: 404"
- 可以多次输入不同ID获取不同帖子内容
代码解析:
useFetch是一个通用的HTTP请求Hook,支持GET、POST等请求方式- 提供了完整的状态管理:
data(响应数据)、loading(加载状态)、error(错误信息) - 支持自动请求和手动请求两种模式(通过
immediate选项控制) - 可以监听URL的变化自动重新请求(如果URL是ref响应式对象)
- 提供了
isIdle和isSuccess计算属性,方便在模板中进行条件渲染 - 支持自定义请求头、请求方法等选项
- 自动处理JSON和文本类型的响应数据
三、组合多个Hook的高级应用
3.1 组合Hook示例:useUserData
// hooks/useValidation.js
import { ref, computed } from 'vue';
// 表单验证Hook
export function useValidation(initialValues = {}, rules = {}) {
// 表单值
const values = ref({ ...initialValues });
// 错误信息
const errors = ref({});
// 验证单个字段
const validateField = (field) => {
const value = values.value[field];
const fieldRules = rules[field] || [];
let errorMessage = '';
// 遍历该字段的所有规则
for (const rule of fieldRules) {
// required规则
if (rule.required && (!value || value === '')) {
errorMessage = rule.message || `${field} is required`;
break;
}
// minLength规则
if (rule.minLength && value.length < rule.minLength) {
errorMessage = rule.message || `${field} must be at least ${rule.minLength} characters`;
break;
}
// maxLength规则
if (rule.maxLength && value.length > rule.maxLength) {
errorMessage = rule.message || `${field} cannot exceed ${rule.maxLength} characters`;
break;
}
// pattern规则(正则表达式)
if (rule.pattern && !rule.pattern.test(value)) {
errorMessage = rule.message || `${field} is invalid`;
break;
}
// 自定义验证函数
if (rule.validator && typeof rule.validator === 'function') {
const result = rule.validator(value, values.value);
if (result !== true) {
errorMessage = result || `${field} is invalid`;
break;
}
}
}
// 更新错误信息
errors.value[field] = errorMessage;
return !errorMessage;
};
// 验证所有字段
const validate = () => {
const fields = Object.keys(rules);
let isValid = true;
// 遍历所有字段进行验证
for (const field of fields) {
const fieldIsValid = validateField(field);
if (!fieldIsValid) {
isValid = false;
}
}
return isValid;
};
// 重置表单
const reset = () => {
values.value = { ...initialValues };
errors.value = {};
};
// 计算属性:是否整体有效
const isValid = computed(() => {
validate();
return Object.values(errors.value).every(error => !error);
});
return {
values,
errors,
isValid,
validate,
validateField,
reset
};
}
// hooks/useUserData.js
import { ref, watch } from 'vue';
import { useFetch } from './useFetch';
import { useValidation } from './useValidation';
// 组合多个Hook,创建用户数据管理Hook
export function useUserData() {
// 用户ID
const userId = ref(null);
// 使用useFetch获取用户数据
const {
data: user,
loading,
error,
fetchData: fetchUser
} = useFetch(null, { immediate: false });
// 用户表单验证规则
const validationRules = {
name: [
{ required: true, message: '姓名不能为空' },
{ minLength: 2, message: '姓名至少2个字符' }
],
email: [
{ required: true, message: '邮箱不能为空' },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '请输入有效的邮箱地址' }
],
age: [
{ required: true, message: '年龄不能为空' },
{
validator: (value) => {
const num = Number(value);
return num >= 18 && num <= 120 || '年龄必须在18-120之间';
}
}
]
};
// 使用useValidation进行表单验证
const {
values: formData,
errors,
isValid,
validate,
reset: resetForm
} = useValidation(
{ name: '', email: '', age: null },
validationRules
);
// 当用户数据加载完成后,更新表单数据
watch(user, (newUser) => {
if (newUser) {
formData.name = newUser.name || '';
formData.email = newUser.email || '';
formData.age = newUser.age || null;
}
});
// 加载用户数据
const loadUser = (id) => {
if (id) {
userId.value = id;
fetchUser(`https://jsonplaceholder.typicode.com/users/${id}`);
}
};
// 保存用户数据
const saveUser = async () => {
// 先验证表单
if (!validate()) {
return false;
}
try {
// 模拟API请求
const response = await fetch(`https://jsonplaceholder.typicode.com/users/${userId.value || ''}`, {
method: userId.value ? 'PUT' : 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
const result = await response.json();
console.log('保存成功:', result);
return true;
} catch (err) {
console.error('保存失败:', err);
return false;
}
};
return {
userId,
user,
formData,
errors,
loading,
error,
isValid,
loadUser,
saveUser,
resetForm,
validate
};
}
在组件中使用:
<template>
<div class="user-data-demo">
<h2>用户数据管理</h2>
<div class="user-controls">
<input v-model="userIdInput" type="number" min="1" max="10" placeholder="用户ID">
<button @click="loadUser(userIdInput)">加载用户</button>
<button @click="resetForm">重置表单</button>
</div>
<div v-if="loading" class="loading">加载中...</div>
<div v-if="error" class="error">错误: {{ error.message }}</div>
<form v-else class="user-form" @submit.prevent="handleSubmit">
<div class="form-group">
<label>姓名:</label>
<input
v-model="formData.name"
@blur="validate('name')"
:class="{ invalid: errors.name }"
>
<span v-if="errors.name" class="error-message">{{ errors.name }}</span>
</div>
<div class="form-group">
<label>邮箱:</label>
<input
v-model="formData.email"
type="email"
@blur="validate('email')"
:class="{ invalid: errors.email }"
>
<span v-if="errors.email" class="error-message">{{ errors.email }}</span>
</div>
<div class="form-group">
<label>年龄:</label>
<input
v-model.number="formData.age"
type="number"
@blur="validate('age')"
:class="{ invalid: errors.age }"
>
<span v-if="errors.age" class="error-message">{{ errors.age }}</span>
</div>
<button
type="submit"
:disabled="!isValid || loading"
class="submit-btn"
>
保存用户
</button>
</form>
</div>
</template>
<script setup>
import { ref } from 'vue';
import { useUserData } from './hooks/useUserData';
// 导入组合后的Hook
const {
userId,
user,
formData,
errors,
loading,
error,
isValid,
loadUser,
saveUser,
resetForm,
validate
} = useUserData();
// 用户ID输入框
const userIdInput = ref('');
// 表单提交处理
const handleSubmit = async () => {
const success = await saveUser();
if (success) {
alert('保存成功!');
} else {
alert('保存失败,请检查表单数据!');
}
};
</script>
<style>
.user-controls {
margin: 15px 0;
display: flex;
gap: 10px;
align-items: center;
}
.loading {
color: #666;
padding: 10px;
}
.error {
color: #d32f2f;
padding: 10px;
background-color: #ffebee;
border-radius: 4px;
}
.user-form {
margin-top: 15px;
border: 1px solid #e0e0e0;
padding: 20px;
border-radius: 4px;
}
.form-group {
margin-bottom: 15px;
}
label {
display: inline-block;
width: 80px;
}
input {
padding: 8px;
width: 250px;
border: 1px solid #ddd;
border-radius: 4px;
}
input.invalid {
border-color: #d32f2f;
}
.error-message {
color: #d32f2f;
font-size: 0.9em;
margin-left: 85px;
display: block;
margin-top: 5px;
}
.submit-btn {
margin-left: 85px;
padding: 8px 20px;
background-color: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.submit-btn:disabled {
background-color: #a0a0a0;
cursor: not-allowed;
}
</style>
运行结果:
- 初始状态显示空表单和用户ID输入区域
- 输入1-10之间的数字并点击"加载用户":
- 显示"加载中…"状态
- 加载完成后,表单会自动填充用户数据
- 表单验证:
- 姓名为空或少于2个字符时,失去焦点会显示错误信息
- 邮箱格式不正确时,显示错误信息
- 年龄不在18-120之间时,显示错误信息
- 表单填写不完整或验证不通过时,"保存用户"按钮禁用
- 点击"保存用户"按钮:
- 表单验证通过后,模拟提交数据
- 提交成功显示"保存成功!"提示
- 提交失败显示"保存失败"提示
- 点击"重置表单"按钮,清空表单数据和错误信息
代码解析:
useUserData组合了useFetch和useValidation两个基础HookuseFetch用于从API获取用户数据和提交用户数据useValidation用于处理表单验证逻辑- 通过
watch监听用户数据变化,自动更新表单 - 提供了完整的用户数据管理功能:加载、验证、保存、重置
- 组件中只需关注UI渲染和用户交互,业务逻辑由Hook封装
四、自定义Hook最佳实践
4.1 命名规范
- 必须以
use开头:遵循Vue的命名约定,便于识别Hook - 使用动词+名词结构:如
useCounter、useFetch、useLocalStorage - 避免过于宽泛的名称:如
useData不如useUserData明确 - 保持一致性:项目内Hook命名风格保持一致
4.2 代码组织
- 单一职责原则:一个Hook只做一件事,便于复用和维护
- 合理拆分:复杂逻辑拆分为多个小Hook,再组合使用
- 目录结构:将Hook集中放在
src/hooks目录下 - 分类组织:可以按功能进一步分类,如
use/validation、use/data-fetching
4.3 状态管理
- 内部状态私有化:不需要暴露给组件的状态应保持私有
- 返回必要的状态:只返回组件需要的状态和方法,避免过度暴露
- 使用ref而非reactive:返回数据时优先使用ref,组件中使用更直观
- 提供修改方法:状态修改应通过提供的方法,而非直接修改返回的ref
4.4 错误处理
- 内部捕获错误:Hook内部适当捕获和处理错误
- 提供错误状态:将错误信息作为状态暴露给组件
- 错误边界:复杂Hook考虑使用错误边界处理异常
- 详细错误信息:提供有意义的错误信息,便于调试
4.5 文档和测试
- 添加JSDoc注释:为Hook及其参数、返回值添加注释
- 示例代码:提供清晰的使用示例
- 单元测试:为Hook编写单元测试,确保可靠性
- 使用说明:说明Hook的用途、参数、返回值和注意事项
4.6 性能优化
- 避免不必要的响应式:非响应式数据不需要用ref/reactive包装
- 合理使用watch选项:使用
deep、immediate等选项优化watch性能 - 缓存计算结果:复杂计算使用
computed缓存结果 - 清理副作用:使用生命周期钩子清理定时器、事件监听等
五、常见自定义Hook示例
5.1 useWindowSize - 监听窗口大小
// hooks/useWindowSize.js
import { ref, onMounted, onUnmounted } from 'vue';
export function useWindowSize() {
// 初始窗口大小
const width = ref(window.innerWidth);
const height = ref(window.innerHeight);
// 窗口大小变化处理函数
const handleResize = () => {
width.value = window.innerWidth;
height.value = window.innerHeight;
};
// 组件挂载时添加事件监听
onMounted(() => {
window.addEventListener('resize', handleResize);
// 初始调用一次,确保尺寸正确
handleResize();
});
// 组件卸载时移除事件监听
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
return {
width,
height,
isMobile: ref(false), // 可以根据需要添加更多派生状态
isTablet: ref(false),
isDesktop: ref(false)
};
}
5.2 useDarkMode - 深色模式切换
// hooks/useDarkMode.js
import { ref, watch, onMounted } from 'vue';
export function useDarkMode() {
// 深色模式状态
const isDarkMode = ref(false);
// 应用深色模式到DOM
const applyDarkMode = (dark) => {
if (dark) {
document.documentElement.classList.add('dark');
localStorage.setItem('darkMode', 'true');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('darkMode', 'false');
}
};
// 切换深色模式
const toggleDarkMode = () => {
isDarkMode.value = !isDarkMode.value;
};
// 组件挂载时初始化
onMounted(() => {
// 从localStorage读取保存的设置
const savedDarkMode = localStorage.getItem('darkMode');
// 检查系统偏好
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
// 确定初始模式
if (savedDarkMode !== null) {
isDarkMode.value = savedDarkMode === 'true';
} else {
isDarkMode.value = prefersDark;
}
// 应用初始模式
applyDarkMode(isDarkMode.value);
});
// 监听模式变化并应用
watch(isDarkMode, (newValue) => {
applyDarkMode(newValue);
});
return {
isDarkMode,
toggleDarkMode
};
}
5.3 useTimer - 定时器Hook
// hooks/useTimer.js
import { ref, onUnmounted, watch } from 'vue';
export function useTimer(initialSeconds = 0, autoStart = false) {
// 定时器状态
const seconds = ref(initialSeconds);
const isRunning = ref(autoStart);
let timerId = null;
// 启动定时器
const start = () => {
if (!isRunning.value) {
isRunning.value = true;
timerId = setInterval(() => {
seconds.value++;
}, 1000);
}
};
// 暂停定时器
const pause = () => {
if (isRunning.value) {
isRunning.value = false;
clearInterval(timerId);
timerId = null;
}
};
// 重置定时器
const reset = (newSeconds = 0) => {
pause();
seconds.value = newSeconds;
};
// 组件卸载时清理定时器
onUnmounted(() => {
if (timerId) {
clearInterval(timerId);
}
});
// 当autoStart变化时自动启动/停止
watch(
() => autoStart,
(newValue) => {
if (newValue && !isRunning.value) {
start();
} else if (!newValue && isRunning.value) {
pause();
}
}
);
// 格式化时间为 HH:MM:SS
const formattedTime = ref('00:00:00');
watch(seconds, (value) => {
const hours = Math.floor(value / 3600).toString().padStart(2, '0');
const minutes = Math.floor((value % 3600) / 60).toString().padStart(2, '0');
const secs = (value % 60).toString().padStart(2, '0');
formattedTime.value = `${hours}:${minutes}:${secs}`;
}, { immediate: true });
return {
seconds,
formattedTime,
isRunning,
start,
pause,
reset
};
}
六、总结
自定义Hook是Vue3组合式API中强大的特性,它提供了一种优雅的方式来封装和复用组件逻辑。通过本文的学习,你应该掌握:
- 自定义Hook的基本概念:以
use开头的函数,用于封装可复用逻辑 - 基础Hook的创建:如计数器、本地存储等简单Hook
- 高级Hook技巧:带参数和选项、异步操作、组合多个Hook
- 最佳实践:命名规范、代码组织、状态管理、错误处理
- 常见Hook示例:窗口大小、深色模式、定时器等实用Hook
合理使用自定义Hook可以显著提高代码复用性和可维护性,使组件代码更加清晰和专注于UI渲染。在实际项目中,建议根据业务需求开发适合的自定义Hook,并遵循本文介绍的最佳实践。
随着对自定义Hook的深入理解和应用,你将能够构建更加模块化、可扩展的Vue3应用程序。
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)