理解Vue3中的hooks(为什么要用hooks)

28,184 阅读11分钟

0.前置知识

0.1 mixin是什么?

mixin,翻译过来就是混入,不仅仅在vue框架中存在mixin,确切的说mixin是一种思想,一种混入的思想。混入的内容就是可以在被混入的地方使用,他会自动的将混入的东西准确的分配到指定的组件中。在vue中,mixin相当于指定混入的变量&函数放入他不混入时候该放的地方。可以认为,vue中的mixin就是相当于组件中的组件。

举个例子:现在组件A中的watch中需要处理的逻辑是hanldleParams, 在组件B中的watch中同样需要这样的逻辑-hanldleParams。那么我们应该如何将这两块相同的逻辑抽象出来复用呢?

那么有两种方法:这两种方法的区别就代表了mixin和utils的区别
1.抽函数:第一种方法就是将hanldleParams以函数的形式抽出来,然后在watch中调用hanldleParams;
2.mixin:上一种抽函数方法虽然可以解决一定的复用问题,但是我们还是需要在组件中写watch,毕竟两个组件都是在watch这个钩子中调用。如果每个组件都写watch,那么watch也是重复的东西,因此mixin就是将watch钩子都可以抽出来的组件,也就是说,mixin抽出来不仅仅是纯函数逻辑,还可以将vue组件特有的钩子等逻辑也可以抽出来,达到进一步复用,这就是mixin的作用。那么组件A\B通过mixin共用一个watch,导入即可,不需要开发人将其放置在指定位置。
特点:Mixin中的数据和方法都是独立的,组件之间使用后是互相不影响的

0.2 mixin解决了什么问题?

mixin解决了两种复用:

  1. 逻辑函数的复用
  2. vue 组件配置复用

注意:组件配置复用是指,组件中的选项式API(例如:data,computed,watch)或者组件的生命周期钩子(created、mounted、destroyed)

0.3 使用&使用场景?

关键:在vue中,mixin定义的就是一个对象,对象中放置的vue组件相应的选项式API和对应的生命周期钩

export const mixins = {
  data() {
    return {};
  },
  computed: {},
  created() {},
  mounted() {},
  methods: {},
};

记住:mixin中一般都是存在vue组件中的选项API和组件生命周期钩子,因为函数的抽象也是要放在组件中特定的API或者钩子中,因此mixin考虑了这一点,直接配置好了所有的API,只要在函数中放置即可。
也就是说,mixin中除了不能把组件中的template模版抽出来,其他任何options-API都可以抽出来放在mixin中(vue2中的所有逻辑无非就是放在data、methods、computed、watch中,这些都可以原封不动放在mixin中)
例如:抽象一个hanldleParams函数,我们一般是在config文件中导出,然后在组件中引入使用,data中method中使用处理data,并且都要设置在data中变量b,那么这段逻辑就可以抽离出来在mixin中放置。而对于与组件业务有关的数据或者逻辑一般都是写在组件中的。

例如:

// 定义一个mixin
export const mixins = {
  data() {
    return {
      msg: "我是小猪课堂",
    };
  },
  computed: {},
  created() {
    console.log("我是mixin中的created生命周期函数");
  },
  mounted() {
    console.log("我是mixin中的mounted生命周期函数");
  },
  methods: {
    clickMe() {
      console.log("我是mixin中的点击事件");
    },
  },
};
//

// src/App.vue中使用导出的mixin
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <button @click="clickMe">点击我</button>
  </div>
</template>

<script>
import { mixins } from "./mixin/index";
export default {
  name: "App",
  mixins: [mixins], // 注册mixin,这样mixin中所有的钩子函数等同于组件中钩子
  components: {},
  created(){
    console.log("组件调用minxi数据",this.msg);
  },
  mounted(){
    console.log("我是组件的mounted生命周期函数")
  }
};
</script>

以上这段代码等价于:

// src/App.vue中使用导出的mixin
<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <button @click="clickMe">点击我</button>
  </div>
</template>

<script>
import { mixins } from "./mixin/index";
export default {
  name: "App",
  mixins: [mixins], // 注册mixin,这样mixin中所有的钩子函数等同于组件中钩子
  data() {
    return {
      msg: "我是小猪课堂",
    };
  },
  // 原来的组件中没有methdos方法,mixin中的方法直接放入组件中
  // 注意:vue中当发生methods函数名和mixin中的methods方法冲突时,解决方案是本组件中优先级高于mixin
  methods: {
    clickMe() {
      console.log("我是mixin中的点击事件");
    },
  },
  created(){
    console.log("我是mixin中的created生命周期函数"); 
    // mixin中的created钩子中的,生命周期中mixin的钩子优先级要高于组件中的优先级
    console.log("组件调用minxi数据",this.msg);
  },
  mounted(){
    // mixin中的 mounted 钩子中的,生命周期中mixin的钩子优先级要高于组件中的优先级
    console.log("我是mixin中的mounted生命周期函数");
    console.log("我是组件的mounted生命周期函数")
  }
};
</script>

注意:mixin中和vue组件中相同的钩子的优先级:

  • mixin中的生命周期函数会和组件的生命周期函数一起合并执行。
  • mixin中的data数据在组件中也可以使用。
  • mixin中的方法在组件内部可以直接调用。
  • 生命周期函数合并后执行顺序:先执行mixin中的,后执行组件的。

此外,mixin对于不同组件的导入,相互之间数据是不会影响的,例如:

// mixin文件
export const mixins = {
  data() {
    return {
      msg: "我是小猪课堂",
    };
  },
  computed: {},
  created() {
    console.log("我是mixin中的created生命周期函数");
  },
  mounted() {
    console.log("我是mixin中的mounted生命周期函数");
  },
  methods: {
    clickMe() {
      console.log("我是mixin中的点击事件");
    },
  },
};

在文件app中使用mixin,并通过changeMsg函数来更改mixin中data的变量msg,此时对于其他组件使用的这个msg变量是不变的。

<template>
  <div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <button @click="clickMe">点击我</button>
    <button @click="changeMsg">更改mixin数据</button>
    <demo></demo>
  </div>
</template>

<script>
import { mixins } from "./mixin/index";
import demo from "./components/demo.vue";
export default {
  name: "App",
  mixins: [mixins],
  components: { demo },
  created() {
    console.log("组件调用minxi数据", this.msg);
  },
  mounted() {
    console.log("我是组件的mounted生命周期函数");
  },
  methods: {
    changeMsg() {
      this.msg = "我是变异的小猪课堂";
      console.log("更改后的msg:", this.msg);
    },
  },
};
</script>

0.4 存在的缺点是什么

mixin
优点:组件中钩子函数的注册复用
缺点:
相同钩子中注册的函数名相同会发生冲突(vue中冲突的解决方案是本组件中优先级高于mixin)
定位错误需要花费时间
滥用会造成维护问题

1.hooks是什么

一般来说,我们开发中会自动抽象出逻辑函数放在utils中,utils中放的纯逻辑,不存在属于组件的东西,例如methods中定义的纯函数等。而hooks就是在utils的基础上再包一层组件级别的东西(钩子函数等)。例如:我们每次点击button都会弹出一个弹窗,自动显示当前日期。但是我将函数放在util中,每次复用都需要click=handleClick 函数放入日期函数,通过handleClick函数管理utils,那么我不如直接将handleClick也封装起来,下次直接调用,复用了methods注册的环节 hooks和utils的区别: hooks中如果涉及到ref,reactive,computed这些api的数据,那这些数据是具有响应式的,而utils只是单纯提取公共方法就不具备响应式,因此可以把hook理解为加入vue3 api的共通方法

2.为什么会出现hooks

那么hooks相当于组件级别的逻辑封装,这种逻辑封装在vue2中的mixin也可以实现,为什么还要使用hooks呢? hooks与mixin的区别
mixin是options API的体现,一个是composition API的体现

1.mixin

  • 在vue2 中有一个东西:Mixins 可以实现这个功能
  • mixins就是将这些多个相同的逻辑抽离出来,各个组件只需要引入mixins,就能实现代码复用
  • 弊端一: 会涉及到覆盖的问题
  • 组件的data、methods、filters会覆盖mixins里的同名data、methods、filters
  • 弊端二:隐式传入,变量来源不明确,不利于阅读,使代码变得难以维护
  • 弊端三:mixin无法传入灵活的传入参数,例如(弊端3的举例1):

我需要定一个一个变量name,但是name的初始值是随机的,那么name定义在mixin中的时候,他的初始化一定是固定的,我们如果要改只能再method中注册一个方法来修改name的值:

例1: 使用mixin的例子

// 混入文件:name-mixin.js
export default {
  data() {
    return {
      name: 'zhng'
    }
  },
  methods: {
    setName(name) {
      this.name = name
    }
  }
}

// 组件:my-component.vue
<template>
  <div>{{ name }}</div>
<template>
<script>
import nameMixin from './name-mixin';
export default {
  mixins: [nameMixin],
  mounted() {
    setTimeout(() => {
      this.setName('Tom') // 通过在组件中调用setName传入参数值,无法传入参数,限制了Mixin的灵活性
    }, 3000)
  }
}
<script>

例2:使用hooks的例子

import { computed, ref, Ref } from "vue"
// 定义hook方法
type CountResultProps = {
    count:Ref<number>;
    multiple:Ref<number>;
    increase:(delta?:number)=>void;
    decrease:(delta?:number)=> void;
}
export default function useCount(initValue = 1):CountResultProps{
    const count = ref(initValue)
    const increase = (delta?:number):void =>{
        if(typeof delta !== 'undefined'){
            count.value += delta
        }else{
            count.value += 1
        }
    }
    const multiple  = computed(()=>count.value * 2)
    const decrease = (delta?:number):void=>{
        if(typeof delta !== "undefined"){
            count.value -= delta
        }else{
            count.value -= 1
        }
    }
    return {
        count,
        increase,
        decrease,
        multiple
    }
 
}

// 在组件中的使用
<template>
   <p>count:{{count}}</p>
   <p>倍数:{{multiple}}</p>
   <div>
     <button @click="increase(10)">加一</button>
     <button @click="decrease(10)">减一</button> // 在模版中直接使用hooks中的方法作为回调函数
   </div>
</template>
<script setup lang="ts">
import useCount from "../views/Hook"
const {count,multiple,increase,decrease}  = useCount(10)
</script>
<style>
 
</style>    

例3: 使用hooks入门的重点例子

使用hooks入门的重点例子: hooks文件一般是都出一个函数,例如:我需要hooks导出一个公用的name变量和setName函数

import {ref} from 'vue'
// 导出1个 name_hooks.ts文件
// hooks中不用写在setup中
export const name_hooks = function(value: string) {
   const name = ref('')
   const setName = (value: string) => {
       name.value = value
   }
   return {
      name, setName
   }
}
// 引入hooks文件
<template>
    <div>{{ name }}</div>
    <select @change="setName"></select> // 这里select组件的change事件会自动传value值
    // 然后value值作为传参传递给setName
</template>
import { name_hooks } from './name_hooks'
export default defineComponent({
    setup() {
       const { name, setName } = name_hooks() // 注意: 通常需要通过解构赋值将需要的属性方法添加进组件中
       return {
          name, setName
       }
    }
})

以上hooks使用方法,常见的操作:
1.导出的hooks是一个函数,函数中可以使用ref,reactive,这样hooks定义的变量和方法如同在组件中一样
2.hooks函数通常返回一个对象,对象中是双向绑定的变量,在vue中间引用的时候第一件事就是解构(vue3中的解构需要注意坑最好复习一下)
export

2.hooks:

  • Vue3中我们可以: 自定义Hook
  • Vue3 的 hook函数 相当于 vue2 的 mixin, 但是: hooks 是函数
  • Vue3 的 hook函数 可以帮助我们提高代码的复用性, 让我们能在不同的组件中都利用 hooks 函数

3.hooks使用场景-自定义hook

3.1 hooks中常见的业务使用场景

重复造轮子的组件,除开一些毫无必要的重复以外,有一些功能组件确实需要封装一下,比如说,一些需要请求后端字典到前端展示的下来选择框,点击之后要展示loading状态的按钮,带有查询条件的表单,这些非常常用的业务场景,我们就可以封装成组件,但是封装成组件就会遇到前面说的问题,每个人的使用习惯和封装习惯不一样,很难让每个人都满意,这种场景,就可以让hook来解决。
(1) 封装一个弹窗,参考文献:juejin.cn/post/695509…

课外知识点1:watch和watchEffect的区别:

image.png

课外知识点2: 浅析使用provide和inject有什么好处:

1 provide & inject的使用场景
使用提示:vue官网中有这样的提示:provide提供和inject注入一般适用于高阶插件和组件库中使用,并不推荐用于一般程序代码中
使用场景:跨级组件通信
一般来说,我们父子组件通信通过prop和emit使用即可,如果通过跨层级或者多个组件节点使用的时候,我们可以通过vuex使用,但是如果不想引入vuex来实现跨层级的话,这个时候可以用provide和inject作为爷孙组件的通信替代方案

2.provide &inject 在 vue2 中的使用方法和需要点

注意点1: provide使用语法
provide的使用语法有两种:一种是赋值对象,另一种是定义一个返回对象的函数
inject使用语法一般是字符串数字,也可以是对象 image.png 注意点2: 响应式问题:provide提供&inject注入的变量并不是响应式的,如果有响应式的需求需要通过引用对象实现 例如:下图中的provide向孙组件提供的变量是简单类型,那么在爷爷组件中进行响应式变换后,孙子组件接受的变量test是不会随之变换的,因为传入的是字符串简单类型。 image.png 而如果爷爷组件提供的provide是引用累心,并且变换的是引用类型内某个属性的值,那么因为孙子组件接受的是对象地址,所以会随之变化,进而达到响应式变换的效果 image.png

总结: vue2中的provide和inject主要就是两块内容:使用语法 和 inject接受的响应式数据问题。

课外知识点3: vue3中如何使用provide和inject:
在vue3中我们应该如何在setup中使用provide和inject呢?

区别:vue3中的provide和inject和vue2的使用是不同的
vue2中provide相当于和data并列的属性,属性值可以是对象也可以是返回对象的函数。可以看出vue2中的provide和inject还是面向过程或者面向对象的,可能inject中不是很明显,因为inject: ['name']接受的是一个数组或者对象inject: {name},但是provide中是provide: {name: '123'} 或者 provide() {return {name: 123}} ,如下的provide和inject: image.png image.png 但是vue3中是使用函数式编程,所以provide&inject都是函数形式提供,我们只需要传入参数即可。

vue3中的使用

提供rovide('name', 'jack')函数:爷爷组件通过provide('name', 'jack'),其中,第一个参数是孙子组件要拿的key,第二个参数是值。
提供inject('name', '默认值:无名'): 孙子组件通过调用inject函数获取爷爷组件的provide的值,其中第一个参数是key,第二个参数式默认值(可选) 参考文献:blog.csdn.net/weixin_5739…

3.2 自定义hook需要满足的规范

image.png

3.3 自定义hooks使用举例

例子1

hooks-1自定义:useCut

import {ref,watch} from "vue"
export function useCut({num1,num2}){
   const cutNum = ref(0);
   watch([num1,num2],(num1,num2)=>{
    cutFunc(num1,num2)
  })
  const cutFunc = (num1,num2)=>{
    cutNum.value = num1+num2
  }
  return {
    cutNum,
    cutFunc
  }
}

hooks2自定义

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


组件中使用自定义hooks

<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>减法等于:{{ cutNum }}</span>
</template>
import { ref } from 'vue'
import useAdd from './addHook.js'     //引入自动hook 
import { useCut } from './cutHook.js' //引入自动hook 
const num1 = ref(2)
const num2 = ref(1)
const { addNum, addFunc } = useAdd({ num1, num2 })  // 加法功能-自定义Hook(将响应式变量或者方法形式暴露出来)
// 因为hooks是函数,不像mixin是对象形式,所以更方便的传入组件中的data变量,交给抽象逻辑使用
addFn(num1.value, num2.value)
const { cutNum, cutFunc } = useCut({ num1, num2 }) // 减法功能-自定义Hook (将响应式变量或者方法形式暴露出来)
subFn(num1.value, num2.value)

3.4 hooks的理解 参考文献:juejin.cn/post/689328…

参考文献:juejin.cn/post/711358… blog.csdn.net/longxiaobao…

blog.csdn.net/qq_39197547…