自定义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使代码更强壮。像写诗一样写代码。