Skip to content

自定义Hook

hook: 直译[hʊk] 钩子
Hook在前端领域并没有明确定义,借用某大佬的定义:在JS里是callback,事件驱动,集成定义一些可复用的方法。

Vue官方文档并没有对自定义Hook做任何定义,却无处不在在使用这个技巧,很多开源项目也在用这个技巧,所以作为一个合格的Vuer学会自定义Hook让Composition Api写起来更丰满是十分必要的!

为什么Vue3要用自定义Hook?: 结论:就是为了让Compoosition Api更好用更丰满,让写Vue3更畅快!像写诗一样写代码! Vue3的优点Composition Api的引入(解决Option Api在代码量大的情况下的强耦合) 让开发者有更好的开发体验。如果正在使用vue2的话,不妨升级到2.7,同样支持自定义hook。

Composition Api彻底摆脱了无脑this的思想:

它的优点之一就是摆脱无脑this导致的强耦合,功能之间互相this,变量和方法在各个方法混杂,无处不在的this是强耦合的,虽然方便,但是碎片化的option api 后期维护是麻烦的。 我相信写Vue2的同学,一定深有感触,一个组件下定义大量变和大量方法,方法嵌套方法,方法之间互相共享变量,维护这样的代码,看似容易理解的Option Api写法,我们需要在methods、data、template之间来回切,Option Api这种写法,代码量和功能小巧时是十分简单明了的,但是代码量一多,功能一复杂,我相信review代码的时候头都痛。 Option Api代码量少还好,代码量多容易导致高耦合!一个组件下含有data 、methods、computed、watch等,同一个功能需要分开写在这些函数上,如果代码量少,那看起来似乎十分明了清晰。一旦代码量大功能复杂,各个功能分开写,维护的时候data 、methods、computed、watch都需要来回切,反而显得过于分散,又高度耦合。 Composition Api解耦Option Api实现低耦合高内聚。如果是Composition Api在功能复杂、代码量巨大的组件下,我们配合自定义Hook,将代码按功能分块写,变量和方法在一起定义和调用,比如A功能下集成了响应式变量和方法,我们后期维护只需要改动A功能模块下的代码,不会像Option Api需要同时关注逻辑分散的methods和data。 所以,要使用Composition Api 自定义Hook必须掌握!它无不体现Composition Api 低耦合高内聚的思想! 笔者在看了官方文档和很多开源项目都是大量使用自定义Hook的!

个人对自定义Hook的理解:

以函数形式抽离一些可复用的方法像钩子一样挂着,随时可以引入和调用,实现高内聚低耦合的目标

封装步骤:

1.将可复用功能抽离为外部JS文件 2.函数名/文件名以use开头,形如:useXX 3.引用时将响应式变量或者方法显式解构暴露出来如:const {nameRef,Fn} = useXX() (在setup函数解构出自定义hook的变量和方法) 举例: 简单的加减法计算,将加法和减法抽离为2个自定义Hook,并且相互传递响应式数据 加法功能-Hook

import { ref, watch } from 'vue';
const useAdd = ({ num1, num2 })  =>{
    const addNum = ref(0)
    watch([num1, num2], ([num1, num2]) => {
        addFn(num1, num2)
    })
    const addFn = (num1, num2) => {
        addNum.value = num1 + num2
    }
    return {
        addNum,
        addFn
    }
}
export default useAdd

减法功能-Hook

//减法功能-Hook
import { ref, watch } from 'vue';
export function useSub  ({ num1, num2 }){
    const subNum = ref(0)
    watch([num1, num2], ([num1, num2]) => {
        subFn(num1, num2)
    })
    const subFn = (num1, num2) => {
        subNum.value = num1 - num2
    }
    return {
        subNum,
        subFn
    }
}

加减法计算组件

<template>
    <div>
        num1:<input v-model.number="num1" style="width:100px" />
        <br />
        num2:<input v-model.number="num2" style="width:100px" />
    </div>
    <span>加法等于:{{ addNum }}</span>
    <br />
    <span>减法等于:{{ subNum }}</span>
</template>
<script setup>
import { ref } from 'vue'
import useAdd from './useAdd.js'     //引入自动hook
import { useSub } from './useSub.js' //引入自动hook
const num1 = ref(2)
const num2 = ref(1)
//加法功能-自定义Hook(将响应式变量或者方法形式暴露出来)
const { addNum, addFn } = useAdd({ num1, num2 })
addFn(num1.value, num2.value)
//减法功能-自定义Hook (将响应式变量或者方法形式暴露出来)
const { subNum, subFn } = useSub({ num1, num2 })
subFn(num1.value, num2.value)
</script>

通过上述示例再来说说自定义Hook和Mixin的关系:

Mixin不足
在 Vue2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:
1、Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。
2、可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。

上面这段是Vue3官方文档的内容,可以概括和补充为:

1、Mixin难以追溯的方法与属性,自定义Hook却可以。

自定义Hook, 引用时将响应式变量或者方法显式暴露出来如:

const {nameRef,Fn} = useXX()

Mixin

export default {
  mixins: [ a, b, c, d, e, f, g ], //一个组件内可以混入各种功能的Mixin
  mounted() {
    console.log(this.name)  //问题来了,这个name是来自于哪个mixin?
  }
}

Mixin不明的混淆,我们根本无法获知属性来自于哪个Mixin文件,给后期维护带来困难

自定义Hook

//加法功能-自定义Hook(将响应式变量或者方法形式暴露出来)
const { addNum, addFn } = useAdd({ num1, num2 })
addFn(num1.value, num2.value)
//减法功能-自定义Hook (将响应式变量或者方法形式暴露出来)
const { subNum, subFn } = useSub({ num1, num2 })
subFn(num1.value, num2.value)

我们很容易看出每个Hook显式暴露出来的响应式变量和方法

2、无法向Mixin传递参数来改变逻辑,但是自定义Hook却可以。

自定义Hook可以灵活传递任何参数来改变它的逻辑,参数不限于其他Hook的暴露出来的变量

Mixin

export default {
  mixins: [ addMixin, subMixin], //组件内混入加法和减法Mixin
  mounted(){
      this.add(num1,num2) //调用addMixin内部的add方法
      this.sub(num1,num2) //调用subMixin内部的sub方法
  }
}

可以通过调用Mixin内部方法来传递参数,却无法直接给Mixin传递参数,因为Mixin不是函数形式暴露的,不发传参

自定义Hook 在上面实例基础上添加个算平均的Hook

//平均功能-Hook
import { ref, watch } from "vue";
export function useAverage(addNum) {
  const averageNum = ref(0);
  watch(addNum, (addNum) => {
    averageFn(addNum);
  });
  const averageFn = (addNum) => {
    averageNum.value = addNum / 2;
  };
  return {
    averageNum,
    averageFn,
  };
}

组件内

//加法功能-自定义Hook(将响应式变量或者方法形式暴露出来)
const { addNum, addFn } = useAdd({ num1, num2 })
addFn(num1.value, num2.value)//主动调用,返回最新addNum
//平均功能-自定义Hook- hook传入参数值来其他hook暴露出来的变量
const { averageNum, averageFn} = useAverage(addNum)
averageFn(addNum.value)

自定义Hook可以灵活传递任何参数来改变它的逻辑,参数不限于其他hook的暴露出来的变量,这提高了Vue在抽象逻辑方面的灵活性。

3、Mixin同名变量会被覆盖,自定义Hook可以在引入的时候对同名变量重命名

Mixin

export default {
  mixins: [ addMixin, subMixin], //组件内混入加法和减法Mixin
  mounted(){
      this.add(num1,num2) //调用加法addMixin内部的add方法
      this.sub(num1,num2) //调用减法subMixin内部的sub方法
  }
}

如果this.add(num1,num2)和 this.sub(num1,num2) 计算的结果返回的同名变量totalNum,由于JS单线程,后面引入的会覆盖前面的,totalNum最终是减法sub的值

自定义Hook

//加法功能-自定义Hook(将响应式变量或者方法形式暴露出来)
const { totalNum:addNum, addFn } = useAdd({ num1, num2 })
addFn(num1.value, num2.value)
//减法功能-自定义Hook (将响应式变量或者方法形式暴露出来)
const { totalNum:subNum, subFn } = useSub({ num1, num2 })
subFn(num1.value, num2.value)

在自定义Hook中,虽然加法和减法Hook都返回了totalNum,但是利用ES6对象解构很轻松给变量重命名。

简单列举一些日常开发中常用的自定义Hook
 /* 可视化大屏自适应缩放 */
//css部分
body.page-pos{
  position: absolute;
  top: 0;
  left:50%;
  overflow: hidden;
  transform-origin: 0 0;
  --un-translate-x: -50%;
  --un-translate-y: 0;
  transform: translateX(var(--un-translate-x)) translateY(var(--un-translate-y));
  overflow: hidden;
}
//js部分
export default function useScalePage(option) {
  onMounted(() => {
    document.body.classList.add('page-pos')
    triggerScale()
    window.addEventListener('resize', triggerScale)
  })
  onUnmounted(() => {
    window.removeEventListener('resize', triggerScale) // 释放
    document.body.classList.remove('page-pos')
    document.body.style = ''
  })
  function triggerScale() {
    // 1.设计稿的尺寸
    const {targetX=1920,targetY=1080}=opstions
    // 2.拿到当前设备(浏览器)的宽度
    const currentX = document.documentElement.clientWidth || document.body.clientWidth
    // 3.计算缩放比例
    const scaleRatioX = currentX / targetX
    document.body.style = `width:${targetX}px; height:${targetY}px; transform: scale(${scaleRatioX}, ${scaleRatioX}) translateX(-50%)`
  }
}
//组件内使用Hook
useScalePage({ targetX: 3840, targetY: 2160 })
/* 结合websocket设置报警事件滑入划出动画*/
//css
@keyframes backInDown {
  0% {
    -webkit-transform: translateY(-200px);
    transform: translateY(-200px);
    opacity: 0.7;
  }
  80% {
    -webkit-transform: translateY(0px);
    transform: translateY(0px);
    opacity: 0.7;
  }
  100% {
    opacity: 1;
  }
}
.animate__backInDown {
  -webkit-animation-name: backInDown;
  animation-name: backInDown;
}
@keyframes backOutDown {
  0% {
    opacity: 1;
  }
  20% {
    -webkit-transform: translateY(0px);
    transform: translateY(0px);
    opacity: 0.7;
  }
  100% {
    -webkit-transform: translateY(200px);
    transform: translateY(200px);
    opacity: 0.7;
  }
}
.animate__backOutDown {
  -webkit-animation-name: backOutDown;
  animation-name: backOutDown;
}
//js
export function useScrollList(props) {
  const { target, size = 2 } = props
  const scrollList = ref([])
  const queueList = []
  let isQueue = false
  const onUpdate = (obj) => {
    if (isQueue) return
    isQueue = true
    if (scrollList.value.length > size - 1) scrollList.value.pop()
    setTimeout(() => {
      isQueue = false
      scrollList.value.unshift(obj)
      setTimeout(() => {
        checkQueueList()
      }, 200)
    }, 400)
  }
  const checkQueueList = () => {
    if (isQueue) return
    const obj = queueList.pop()
    if (obj) onUpdate(obj)
  }
  watch(target, (newValue) => {
    queueList.unshift(newValue)
    checkQueueList()
  })
  return scrollList
}
//组件内使用
<transition-group tag="ul" enter-active-class="animate__animated animate__backInDown" leave-active-class="animate__animated animate__backOutDown">
  <li v-for="item in scrollList" :key="item.id" class="">
    <img :src="item.img" alt="">
  </li>
</transition-group>

const { person, setData } = toRefs(useSocketStore())
const scrollList = useScrollList({ target: person, size: 2 })
总结:

Option Api中,data、methods、watch.....分开写,这种是碎片化的分散的,代码一多就容易高耦合,维护时来回切换代码是繁琐的! Composition Api,通过利用各种自定义Hook将碎片化的响应式变量和方法按功能分块写,实现高内聚低耦合 形象的讲法:自定义Hook是组件下的函数作用域的,而Mixin是组件下的全局作用域。全局作用域有时候是不可控的,就像var和let这些变量声明关键字一样,const和let是var的修正。Composition Api正是对Option Api 高耦合和随处可见this的黑盒的修正,自定义Hook是一种进步。 把Mixin和自定义Hook进行比较,一个是Option Api的体现,一个是Composition Api的体现。如果能理解高内聚低耦合的思想,那么就能理解为什么Vue3是使用Composition Api,并通过各种自定义Hook使代码更强壮。像写诗一样写代码。