# 前言
作为前端开发我们可能了解到 Hooks 最多都是在 React 或者 Vue 中,但是在编程领域, Hooks 是一个通用术语,通常指的是允许开发者在特定时刻插入自定义代码或逻辑的一种机制。 Hooks 的概念广泛应用于多种场景和技术中,比如在前端框架、后端框架、操作系统、游戏开发引擎等。
但是 Hooks 在我们前端开发领域中还是赋予了一些特定的意义,它是一种更灵活和模块化的方式来编写组件逻辑,可以更好地复用和组织代码。它们使得组件的逻辑更加简洁、模块化和易于维护。
# 来一个 Option Api 和 Composition Api 的对比
讲 Vue3 Hooks 前,我们先来看下 Option Api 和 Composition Api 对比。
| 特性 | Option API | Composition API | 
|---|---|---|
| 定义组件 | 通过 data,methods,computed,watch分别定义组件的状态、方法、计算属性和监听器 | 通过 setup函数,使用 Vue 的组合函数(如ref,reactive,computed,watch)定义组件的所有逻辑 | 
| 代码组织 | 按功能(如 data、methods 等)分组,结构化明了 | 按逻辑单元分组,相关逻辑聚合在一起,便于维护和复用 | 
| 可读性和可维护性 | 对于小型组件较好,但复杂组件的逻辑分散在各个部分,难以维护 | 对于复杂组件更优,逻辑单元清晰,便于理解和维护 | 
| 类型支持 | 依赖外部工具(如 TypeScript)进行类型检查和推断 | 原生支持类型,TypeScript 推断更好,类型安全更高 | 
| 逻辑复用 | 使用 mixins复用逻辑,但可能导致命名冲突和难以调试 | 使用 hooks或composable函数复用逻辑,无命名冲突,代码更清晰 | 
| this | 在 data,methods,computed,watch等中使用this引用组件实例 | 在 setup中没有this,使用组合函数返回的对象直接引用 | 
| 维护 | 按功能分组,虽然结构化明了,但逻辑会分散在不同部分,不利于维护 | 可以轻松将同一个逻辑关注点相关的代码放在一起,直观明了,而且可以很轻松地将这一组代码移动到一个外部文件 | 
| 性能优化 | 需要手动处理,如深度监听响应式数据等 | 内置优化,如自动依赖收集,更高效 | 
| 打包体积 | 打包体积相对较大,包含较多的 runtime 代码 | 打包体积更小,树摇优化(Tree Shaking)更有效,减少未使用代码 | 
# Hooks 命名规范
下面的这些规范,都只是建议性规范,并非强制性。
- 为了保持一致性和可读性,所有的自定义 hooks 应该以 use开头。这可以让开发者一眼识别出这是一个 hook。
- 命名应该尽量描述 hook 的功能或用途,以便其他开发者能够快速理解这个 hook 的作用。
- 通常我们会用名词或名词短语来命名,而不是动词。例如, useUserData比useGetUserData更合适。
- 每个 hook 应该尽量只做一件事,这样可以让 hook 更加专注和可重用。如果一个 hook 承担了多个职责,考虑将其拆分成多个更小的 hooks。
- 命名时要避免使用与 Vue 已有功能冲突的名称,例如 useData,useMethods等,这些名称可能会引起混淆。
# 在 Vue3 中的 Hooks
官方对自定义 hooks 定义:在 Vue 应用的概念中,“组合式函数” (Composables) 是一个利用 Vue 组合式 API 来封装和复用有状态逻辑的函数。
Vue3 中为什么大力推崇使用 Hooks , 这得益于 Vue3 Composition API 提供了通过函数组织组件逻辑的方式。它通过 setup 函数作为组件的入口点,允许你在函数中定义组件的状态、计算属性和副作用。 setup 函数在组件实例创建之前调用,并返回一个对象,组件模板中的属性和方法都会从这个对象中获得。
这里我们来一个非常简单的 Hooks 使用例子。
| // useCounter.ts | |
| import { ref } from 'vue'; | |
|  /** | |
| * useCounter 函数用于创建和管理一个计数器 | |
| * | |
| */ | |
| export function useCounter() { | |
| const count = ref(0); | |
| const increment = () => { | |
| count.value++; | |
| }; | |
|     // 返回一个包含 count 和 increment 的对象,供外部使用 | |
| return { | |
|         count, | |
| increment | |
| }; | |
| } | 
在组件中使用
| <template> | |
|   <div > | |
| <p></p> | |
| <button @click="increment">Increment</button> | |
|   </div> | |
| </template> | |
| <script lang="ts" setup> | |
| import { useCounter } from './useCounter.ts'; | |
| const { count, increment } = useCounter(); | |
| </script> | 
useCounter.ts 提供了一个简单易用的计数器逻辑片段,可以轻松地集成到 Vue组件 中。
# 为什么在 Vue3 中就基本看不到 Mixin 了?
这里我们上一个奇怪的例子来直接说明在之前 Vue2 Mixin 开发容易遇到的问,注意下面代码是有省略简写的。
| // 第一个 Mixin | |
| const mixinA = { | |
| data() { | |
| return { | |
| message: '来自 Mixin A' | |
| }; | |
| }, | |
| methods: { | |
| showMessage() { | |
| console.log(this.message); | |
|     } | |
|   } | |
| }; | |
| // 第二个 Mixin | |
| const mixinB = { | |
| data() { | |
| return { | |
| messageB: '来自 Mixin B' | |
| }; | |
| }, | |
| methods: { | |
| showMessage() { | |
| this.messageA= '来自messageB' | |
| console.log(this.message); | |
|     } | |
|   } | |
| }; | |
| // 第三个 Mixin | |
| const mixinC = { | |
| mixins: [mixinA], | |
| data() { | |
| return { | |
| messageC: '来自 Mixin C' | |
| }; | |
| }, | |
| methods: { | |
| showMessage() { | |
| this.message= '来自messageC' | |
| console.log(this.message); | |
|     } | |
|   } | |
| }; | |
| // 使用这两个 Mixin 的组件 | |
| export default { | |
| mixins: [mixinB, mixinC], | |
| created() { | |
| console.log(this.message) // 这里的 message 来自哪里? | |
| this.showMessage(); // 这里的 showMessage 方法到底调用的谁的 | |
| console.log(this.message) // 这里的 message 结果是啥? | |
|   } | |
| }; | 
通过上面例子我们不难发现 Mixin 有很严重的缺点:
- 如果同时混入有链式或者多个 Mixin, 你会发现属性和方法难以追溯,时间久了需要一个一个去点开看。
- 如果有相同的属性名和方法名,会被覆盖,产生意想不到 bug。
- 无法向 Mixin 传递参数来改变逻辑。
再来看看用 Vue3 Composition API Hooks 是如何完美解决这个问题的,注意下面代码是有省略简写的
| <template> | |
| <div> | |
| <p><!--swig1--></p> | |
| <p><!--swig2--></p> | |
| <button @click="handleMessages">Show Messages</button> | |
| </div> | |
| </template> | |
| <script setup> | |
| import { ref } from 'vue'; | |
| // 第一个 hooks 组件 useMixinA | |
| function useMixinA() { | |
| const message = ref('来自 Hook A'); | |
| function showMessage() { | |
| console.log(message.value); | |
|   } | |
| return { message, showMessage }; | |
| } | |
| // 第二个 hooks 组件 useMixinB | |
| function useMixinB() { | |
| const message = ref('来自 Hook B'); | |
| function showMessage() { | |
| console.log(message.value); | |
|   } | |
| return { message, showMessage }; | |
| } | |
| // 第三个 hooks 组件 useMixinC | |
| function useMixinC(num) { | |
| const message = ref('来自 Hook C'); | |
| function showMessage() { | |
| console.log(num); | |
|   } | |
| return { message, showMessage }; | |
| } | |
| const { message: messageA, showMessage: showMessageA } = useMixinA(); | |
| const { message: messageB, showMessage: showMessageB } = useMixinB(); | |
| const { message: messageC, showMessage: showMessageC } = useMixinC(1); | |
| function handleMessages() { | |
| showMessageA(); // 输出 ' 来自 Hook A' | |
| showMessageB(); // 输出 ' 来自 Hook B' | |
| showMessageC(); // 输出 1 | |
| console.log(messageA.value); // 输出 ' 来自 Hook A' | |
| console.log(messageB.value); // 输出 ' 来自 Hook B' | |
| console.log(messageC.value); // 输出 ' 来自 Hook C' | |
| } | |
| </script> | |
| <style scoped> | |
| button { | |
| margin-top: 10px; | |
| } | |
| </style> | 
通过上面例子我们可以很清晰的看到 Composition API 自定义Hooks 的优点
- 通过解构赋值可以给每个 hook 返回的属性和方法起别名,这样避免了命名冲突。
- 可以很轻松的追溯到属性和方法的来源。
- 可以灵活传递通过任何参数来改变它的逻辑,这大大提高了 Vue3 在抽象逻辑方面的灵活性。
# Vue3 Hooks vs React Hooks
- Vue3 Hooks主要强调- 模块化与复用性:通过组合函数,开发者可以将逻辑提取到独立的函数中,提升代码的可读性和复用性。- 灵活性:它提供了更灵活的方式来组织和管理组件逻辑,特别是对于复杂的组件。
- React Hooks主要强调- 函数式编程(函数组件): 提供了在函数组件中使用状态和其他 React 特性的能力,消除了类组件的复杂性。React Hooks 提供了在函数组件中使用状态和其他 React 特性的能力,消除了类组件的复杂性。- 简洁与优雅:它简化了组件的状态管理和副作用处理,使代码更简洁、更易读。
来看一个 React Hooks 的实际例子
| import React, { useState, useEffect } from 'react'; | |
| function Timer() { | |
| const [count, setCount] = useState(0); | |
| useEffect(() => { | |
| const timer = setInterval(() => { | |
| setCount(prevCount => prevCount + 1); | |
| }, 1000); | |
| return () => clearInterval(timer); // 清除副作用 | |
| }, [count]); // 只有 数组里面的 count 变化了 useEffect 才会调用 | |
|    // 模拟一个耗时的计算 | |
| const computeFactorial = (n) => { | |
| console.log('Computing factorial...'); | |
| if (n <= 1) return 1; | |
| return n * computeFactorial(n - 1); | |
| }; | |
|   // 使用 useMemo 来缓存计算结果 | |
| const factorial = useMemo(() => computeFactorial(count), [count]); | |
| return <div>{message} {factorial}</div>; | |
| } | |
| export default Timer; | 
代码解释:
- 清除副作用:在 Timer组件中,我们使用useEffect来设置一个计时器,并在返回的清除函数中使用clearInterval来清除这个计时器,从而避免内存泄漏和其他潜在问题。
- 使用 useEffect 第二个参数 优化性能: 只有 数组里面的 count 变化了 useEffect 才会调用。
- 使用 useMemo优化性能:在ExpensiveComputation组件中,我们定义了一个耗时的计算函数computeFactorial。通过useMemo钩子,我们可以将这个计算结果缓存起来,只有当依赖项count发生变化时才重新计算,从而避免了不必要的重新计算。
# 实战应用 - 自定义 Hooks
来一个比较常用的关于列表请求的 Hooks 封装,为了方便大家理解所以功能只封装了比较典型的使用场景,实际操作中还可以加入请求参数拦截修改等等功能。
下面代码主要用到了 ElementPlus Vue3 , 首先新建一个 hooks 文件 useTable.ts
| import { ref, reactive } from 'vue'; | |
| import { debounce } from "lodash-es" | |
| interface TableType { | |
| manual?: boolean; // 是否手动触发 | |
| requestFn: Function,// 请求接口方法 | |
| defaultParams?: Record<string, any> // 默认参数 | |
| } | |
| export function useTable({ | |
|     requestFn, | |
|     manual, | |
| defaultParams | |
| }: TableType) { | |
| const tableData = ref([]) | |
| const pagination = reactive({ | |
| pageSize: 10, | |
| currentPage: 1, | |
| total: 0 | |
| }) | |
| const queryTableData = async (param?: Record<string, any>) => { | |
| let params: any = { | |
| pageSize: pagination.pageSize, | |
| currentPage: pagination.currentPage, | |
| ...defaultParams, | |
|             ...param | |
| }; | |
| console.log('params', params) | |
| const result = await requestFn(params) | |
| if (result) { | |
| tableData.value = result.list | |
| pagination.total = result.total | |
|         } | |
|     } | |
|     // 添加防抖 | |
| const debouncedSearch = debounce((params?: Record<string, any>) => queryTableData(params), 500); | |
|     // 页数切换 | |
| function handleCurrentChange(current: number) { | |
| pagination.currentPage = current | |
| debouncedSearch(); | |
|     } | |
|     //size 切换 | |
| function handleSizeChange(size: number) { | |
| pagination.pageSize = size; | |
| debouncedSearch(); | |
|     } | |
|     // 条件 查询 | |
| function querySearch(params?: Record<string, any>) { | |
| debouncedSearch(params); | |
|     } | |
| if (!manual) { | |
|         // 自动执行 | |
| debouncedSearch() | |
|     } | |
| return { | |
|         tableData, | |
|         pagination, | |
|         querySearch, | |
|         handleSizeChange, | |
|         handleCurrentChange, | |
|         queryTableData, | |
| }; | |
| } | 
再来创建一个 Vue 文件引入我们的 hooks
| <template> | |
| <div> | |
| <el-form :inline="true" :model="formInline" class="demo-form-inline"> | |
| <el-form-item label="姓名"> | |
| <el-input | |
| v-model="formInline.user" | |
| placeholder="请输入姓名" | |
| clearable | |
| /> | |
| </el-form-item> | |
| <el-form-item label="城市"> | |
| <el-select | |
| v-model="formInline.region" | |
| placeholder="请选择城市" | |
| style="width: 200px;" | |
| clearable | |
|         > | |
| <el-option label="Zone one" value="上海" /> | |
| <el-option label="Zone two" value="北京" /> | |
| </el-select> | |
| </el-form-item> | |
| <el-form-item label="时间"> | |
| <el-date-picker | |
| v-model="formInline.date" | |
| type="date" | |
| placeholder="请选择时间" | |
| clearable | |
| /> | |
| </el-form-item> | |
| <el-form-item> | |
| <el-button type="primary" @click="querySearch(formInline)">查询</el-button> | |
| </el-form-item> | |
| </el-form> | |
| <el-table :data="tableData" border style="width: 100%"> | |
| <el-table-column prop="user" label="姓名" /> | |
| <el-table-column prop="region" label="城市" /> | |
| <el-table-column prop="date" label="时间" /> | |
| </el-table> | |
| <div style="display: flex;justify-content: flex-end;padding: 10px;"> | |
| <el-pagination | |
| v-model:current-page="pagination.currentPage" | |
| :page-size="pagination.pageSize" | |
| layout="total, sizes, prev, pager, next, jumper" | |
| :total="pagination.total" | |
| @size-change="handleSizeChange" | |
| @current-change="handleCurrentChange" | |
| /> | |
| </div> | |
| </div> | |
| </template> | |
| <script lang="ts" setup> | |
| import {reactive} from "vue"; | |
| import {useTable} from './useTable.ts' | |
| const formInline = reactive({ | |
| user: "", | |
| region: "", | |
| date: "", | |
| }); | |
| // 模拟接口请求 | |
| const requestFn = ()=>{ | |
| return new Promise((resolve)=>{ | |
| const total = Math.floor(Math.random() * 101) // 生成随机数 0 -100 | |
| const list = [] | |
| for (let index = 0; index < 10; index++) { | |
| list.push({ | |
| user:"测试"+index, | |
| region:2, | |
| date:"2024-07-31" | |
| }) | |
|     } | |
| const resultData={ | |
|       total, | |
| size:10, | |
|       list:list | |
|     } | |
| resolve(resultData) | |
| }) | |
| } | |
| const {tableData,pagination,handleSizeChange,handleCurrentChange,querySearch} = useTable({requestFn,defaultParams:{age:1}}) | |
| </script> | 
代码功能讲解:
- 把页面列表查询等比较常用的功能封装到 Hooks 里面,方便其他页面引用。
- 把页码切换,size 切换功能等也封装过去。
- 利用 lodashdebounce函数给我们请求接口添加防抖功能,防止频繁请求。
# 总结
通过上面内容 我们可以了解到 Vue3 hooks 提供了一种灵活高效的方式来管理组件的状态和逻辑。相比 Vue 2 的 Options API , Composition API 和 hooks 提供了更强的功能,避免了命名冲突和逻辑混乱。通过 自定义 hooks ,开发者可以更方便地复用逻辑和管理复杂组件。
作为前端开发我认为掌握 hooks 的用法,和思想是必不可少的,当然现在也有很多比较成熟的 Hooks 库,例如 ahooks 和 vueuse 等,推荐学习掌握。
