深入了解 Vue3 之 你必学会的技能-自定义Hooks

前言

作为前端开发我们可能了解到Hooks最多都是在React或者Vue中,但是在编程领域,Hooks 是一个通用术语,通常指的是允许开发者在特定时刻插入自定义代码或逻辑的一种机制。Hooks 的概念广泛应用于多种场景和技术中,比如在前端框架、后端框架、操作系统、游戏开发引擎等。

但是Hooks在我们前端开发领域中还是赋予了一些特定的意义,它是一种更灵活和模块化的方式来编写组件逻辑,可以更好地复用和组织代码。它们使得组件的逻辑更加简洁、模块化和易于维护。

来一个Option ApiComposition Api 的对比

Vue3 Hooks前,我们先来看下Option ApiComposition Api 对比。

特性 Option API Composition API
定义组件 通过 data, methods, computed, watch 分别定义组件的状态、方法、计算属性和监听器 通过 setup 函数,使用 Vue 的组合函数(如 ref, reactive, computed, watch)定义组件的所有逻辑
代码组织 按功能(如 data、methods 等)分组,结构化明了 按逻辑单元分组,相关逻辑聚合在一起,便于维护和复用
可读性和可维护性 对于小型组件较好,但复杂组件的逻辑分散在各个部分,难以维护 对于复杂组件更优,逻辑单元清晰,便于理解和维护
类型支持 依赖外部工具(如 TypeScript)进行类型检查和推断 原生支持类型,TypeScript 推断更好,类型安全更高
逻辑复用 使用 mixins 复用逻辑,但可能导致命名冲突和难以调试 使用 hookscomposable 函数复用逻辑,无命名冲突,代码更清晰
this data, methods, computed, watch 等中使用 this 引用组件实例 setup 中没有 this,使用组合函数返回的对象直接引用
维护 按功能分组,虽然结构化明了,但逻辑会分散在不同部分,不利于维护 可以轻松将同一个逻辑关注点相关的代码放在一起,直观明了,而且可以很轻松地将这一组代码移动到一个外部文件
性能优化 需要手动处理,如深度监听响应式数据等 内置优化,如自动依赖收集,更高效
打包体积 打包体积相对较大,包含较多的 runtime 代码 打包体积更小,树摇优化(Tree Shaking)更有效,减少未使用代码

这里我们借鉴一下官网的对比图

通过上面内容可以看到,如果是比较复杂的功能,我们利用Composition Api 将功能和逻辑点分组分块写在一起,如果有些功能逻辑能够复用,然后配合后文我们讲到的自定义Hooks就可以轻松驾驭,写出高性能,便于维护的代码。

Hooks命名规范

下面的这些规范,都只是建议性规范,并非强制性。

  • 为了保持一致性和可读性,所有的自定义hooks应该以use开头。这可以让开发者一眼识别出这是一个hook。
  • 命名应该尽量描述hook的功能或用途,以便其他开发者能够快速理解这个hook的作用。
  • 通常我们会用名词或名词短语来命名,而不是动词。例如,useUserDatauseGetUserData更合适。
  • 每个hook应该尽量只做一件事,这样可以让hook更加专注和可重用。如果一个hook承担了多个职责,考虑将其拆分成多个更小的hooks。
  • 命名时要避免使用与Vue已有功能冲突的名称,例如useDatauseMethods等,这些名称可能会引起混淆。

在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
    };
}

在组件中使用

vue 复制代码
<template>
  <div >
     <p>{{ count }}</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开发容易遇到的问,注意下面代码是有省略简写的。

javascript 复制代码
// 第一个 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 是如何完美解决这个问题的,注意下面代码是有省略简写的

xml 复制代码
<template>
  <div>
    <p>{{ messageA }}</p>
    <p>{{ messageB }}</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 的实际例子

javascript 复制代码
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

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

ini 复制代码
<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 切换功能等也封装过去。
  • 利用 lodash debounce函数给我们请求接口添加防抖功能,防止频繁请求。

总结

通过上面内容 我们可以了解到Vue3 hooks 提供了一种灵活高效的方式来管理组件的状态和逻辑。相比 Vue 2Options APIComposition APIhooks 提供了更强的功能,避免了命名冲突和逻辑混乱。通过 自定义 hooks,开发者可以更方便地复用逻辑和管理复杂组件。

作为前端开发我认为掌握 hooks 的用法,和思想是必不可少的,当然现在也有很多比较成熟的Hooks库,例如 ahooksvueuse等,推荐学习掌握。

相关推荐
真滴book理喻1 小时前
Vue(四)
前端·javascript·vue.js
越甲八千1 小时前
重温设计模式--享元模式
设计模式·享元模式
不是鱼3 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
码农爱java3 小时前
设计模式--抽象工厂模式【创建型模式】
java·设计模式·面试·抽象工厂模式·原理·23种设计模式·java 设计模式
开心工作室_kaic3 小时前
springboot476基于vue篮球联盟管理系统(论文+源码)_kaic
前端·javascript·vue.js
川石教育3 小时前
Vue前端开发-缓存优化
前端·javascript·vue.js·缓存·前端框架·vue·数据缓存
搏博3 小时前
使用Vue创建前后端分离项目的过程(前端部分)
前端·javascript·vue.js
isSamle3 小时前
使用Vue+Django开发的旅游路书应用
前端·vue.js·django
越甲八千3 小时前
重温设计模式--中介者模式
windows·设计模式·中介者模式
犬余4 小时前
设计模式之桥接模式:抽象与实现之间的分离艺术
笔记·学习·设计模式·桥接模式