Vue3中编写自定义的指令,结合真实的场景实现点击指令绑定元素之外区域触发事件

776 阅读7分钟

af2212ea45068d4b5f83378fcc72d14a.gif vue3官网自定义指令官方文档。点击可参阅

这个例子涉及自定义指令编写、全局注册、和指令绑定元素之外的区域点击的实现。

在vue3中,如果script标签设置了setup属性,则创建局部指令可以可以直接在script标签中编写代码,官方给的例子如下:

<script setup>
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

当然也可以将vFocus这个对象编写在外部的js/ts文件中,直接import vFocus from 'xxx'.

对于自定义指令它也有一套和vue生命周期差不多的钩子函数,这里只涉及到两个,其他的可以参阅官方文档.这些函数中都有几个一样的参数,我们可以通过这些参数拿到当前指令绑定的真实DOM元素和指令上的修饰符等。这些已经能满足当前例子的需要了。

以我这个使用场景为例,我想做的是页面中有一个按钮,当我给按钮绑定指令v-click-outside指令,按钮点击的时候修改触发点击事件完成对按钮字体颜色的修改,当点击外部区域时,触发事件修改按钮字体颜色这么一个功能,这个场景往往也用在关闭下拉列表、弹框之类的。

先给html写好,详看注释

<script setup lang="ts">
// 导入类型描述
import type { VueElement } from 'vue';
// 还没编码呢,先给文件引入,文件中导出的对象已经定义好了
import vClickOutside from '../v-click-outside';

  defineProps<{
    msg: string;
  }>();
/**
  * 绑定指令后点击自身和外部区域的事件函数,
  * 这里我希望通过指令的功能完成对按钮样式的修改
  * @param type - 点击外部outside还时自身self
  * @param e - 鼠标的点击event参数
  * @param el - 指令绑定的元素
  */
  const onClickOutside = (type: string, e: Event, el: VueElement) => {

    if (type === 'self') {
      el.style.color = 'red'
    } else {
      el.style.color = ''
    }
  }

</script>

<template>
  <div class="greetings">
  <!--这里我设置传给指令的参数[msg],这个参数可以是动态的某个变量,一个修饰符delay,修饰符设置点击后一秒触发事件-->
    <h1 v-click-outside:[msg].delay="onClickOutside" class="green">{{ msg }}</h1>

    <h1 v-click-outside="onClickOutside">再试试我</h1>
  </div>
</template>

编写v-click-outside.ts:

import type { VueElement, DirectiveBinding } from 'vue';
/**
  * 这里定义Map集合用来存放自定义事件的列表,
  * 用于存放指令给元素和document上添加的自定义事件
  * 当指令卸载时能够移除对应的自定义事件,后续会用到
  * 想了想其实将事件挂到对应的元素对象上应该最方便
  */
const destroyMap = new Map();
// 导出这个自定义指令的对象
export default {
  /**
  * @param el - 当前指令绑定的元素的真实Dom
  * @param binding - 指令上绑定的参数,包含指令传入的value,和指令上的带的参数、修饰符等。
  * mounted钩子还有vnode, prevVnode这两个参数
  */
  mounted: (el: VueElement, binding: DirectiveBinding) => {

    /**
      * @param binding - 包含一下参数信息
      * value: 传递给指令的值,这个例子中的vlaue就是onClickOutside这个函数。
      * oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
      * arg:传递给指令的参数,(如果有的话),这个例子中就是传入的变量msg对应的字符串。
      * modifiers:一个包含修饰符的对象 (如果有的话)。这个例子中 v-click-outside:[msg].delay中,修饰符对象是 { delay: true},当然这可以设置多个。
      * instance:使用该指令的组件实例。
      * dir:指令的定义对象,就是当前这个export default 的对象。
      */
    const { value, arg, modifiers: { delay }} = binding;
    /**
     * 组件挂载完成后给当前绑定指令的元素设只点击事件
     * 这里必须要定义一个外部的回调函数,因为指令卸载后要移除它
     */
    el.addEventListener("click", onClickSelf, false);

  },

  unmounted: (el: VueElement) => {

  }
}

一个自定义指令的对象骨架定义好了,继续编写指令的实际功能,点击绑定指令元素触发事件和点击该元素外部区域触发对事件.

上一步中已经给当前元素设置了自定义点击事件onClickSelf

/**
 * @param e - 给上层传当前鼠标事件的event
 */
function onClickSelf(e: Event) {
      // 这里做了一个小判断,如果给元素对应的外部区域存在自定义的点击事件则不允许再次点击,这里可以忽略
      if (typeof destroyMap?.get?.(el)?.[0] === 'function') return;
      
      let timer = null;
      
      // 判断传入的value是否为一个函数
      if (typeof value === 'function') {
        // 延迟执行的修饰符delay逻辑
        if (delay) {
          timer = setTimeout(() => {
            value('self', e, el, arg)
          }, 1000)
        } else {
          value('self', e, el, arg)
        }
      }
      /**
       * 点击自身的事件已经触发,设置点击外部区域的事件,即给document设置点击事件
       */
      document.addEventListener('click', onClickOutside, false);
      /**
        * 组件挂载完成后给当前绑定指令的元素设只点击事件
        * 这里必须要定义一个外部的回调函数,因为指令卸载后要移除它
        */
      destroyMap.set(el, [onClickOutside, onClickSelf, timer])
    };
    
    // 点击外部区域的事件
    function onClickOutside(e: Event) {
      /**
       * 点击外部区域通过鼠标实际点击元素的target判断是否是指令绑定的元素或这他的子元素
       * 如果是指令绑定的元素或者是它的子元素,则点击区域不是el外部
       */
      const { target } = e;

      if (target !== el && !el.contains(target as VueElement)) {
        // 同样判断value是否为函数类型
        if (typeof value === 'function') {
          value('outside', e, el, arg)
        };
        // 外部区域点击后,移除onClickOutside,
        document.removeEventListener('click', destroyMap.get(el)[0], false);
        // 并删除记录的这一项目
        destroyMap.get(el)[0] = null;
        // 清除定时器
        if (destroyMap.get(el)?.[2]) clearTimeout(destroyMap.get(el)[2]);
        destroyMap.get(el)[2] = null;
      };

    }

最后组件卸载时移除el关联的自定义鼠标事件,至此完整的v-click-outside.ts代码如下:

import type { VueElement, DirectiveBinding } from 'vue';

const destroyMap = new Map();

export default {

  /**
  * @param el - 当前指令绑定的元素的真实Dom
  * @param binding - 指令上绑定的参数,包含指令传入的value,和指令上的带的参数、修饰符等。
  * mounted钩子还有vnode, prevVnode这两个参数
  */
  mounted: (el: VueElement, binding: DirectiveBinding) => {

    /**
      * @param binding - 包含一下参数信息
      * value: 传递给指令的值,这个例子中的vlaue就是onClickOutside这个函数。
      * oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
      * arg:传递给指令的参数,(如果有的话),这个例子中就是传入的变量msg对应的字符串。
      * modifiers:一个包含修饰符的对象 (如果有的话)。这个例子中 v-click-outside:[msg].delay中,修饰符对象是 { delay: true},当然这可以设置多个。
      * instance:使用该指令的组件实例。
      * dir:指令的定义对象,就是当前这个export default 的对象。
      */
    const { value, arg, modifiers: { delay }} = binding;
    // 点击外部区域的事件
    function onClickOutside(e: Event) {
      /**
       * 点击外部区域通过鼠标实际点击元素的target判断是否是指令绑定的元素或这他的子元素
       * 如果是指令绑定的元素或者是它的子元素,则点击区域不是el外部
       */
      const { target } = e;

      if (target !== el && !el.contains(target as VueElement)) {
        // 同样判断value是否为函数类型
        if (typeof value === 'function') {
          value('outside', e, el, arg)
        };
        // 外部区域点击后,移除onClickOutside,
        document.removeEventListener('click', onClickOutside, false);
        // 并删除记录的这一项目
        destroyMap.get(el)[1] = null;
      };

    }

    /**
     * @param e - 给上层传当前鼠标事件的event
     */
    function onClickSelf(e: Event) {
      // 这里做了一个小判断,如果给元素对应的外部区域存在自定义的点击事件则不允许再次点击,这里可以忽略
      if (typeof destroyMap?.get?.(el)?.[1] === 'function') return;

      // 判断传入的value是否为一个函数
      if (typeof value === 'function') {
        // 延迟执行的修饰符delay逻辑
        if (delay) {
          setTimeout(() => {
            value('self', e, el, arg)
          }, 1000)
        } else {
          value('self', e, el, arg)
        }
      };
      /**
       * 点击自身的事件已经触发,设置点击外部区域的事件,即给document设置点击事件
       */
      document.addEventListener('click', onClickOutside, false);
      // 将这两个事件存放在map中该元素对应的数组中,后续指令卸载需要使用
      destroyMap.set(el, [onClickOutside, onClickSelf])
    };

    /**
     * 组件挂载完成后给当前绑定指令的元素设只点击事件
     * 这里必须要定义一个外部的回调函数,因为指令卸载后要移除它
     */
    el.addEventListener("click", onClickSelf, false);

  },

  unmounted: (el: VueElement) => {
    // 卸载指令后移除el关联的所有自定义事件,并从记录中删除
    const [f1, f2] = destroyMap.get(el);

    document.removeEventListener('click', f1, false);
    el.removeEventListener('click', f2, false);

    destroyMap.delete(el);
  }
}

指令的编码结束!但是目前这还是一个局部指令,哪里要用还需要子啊文件中import,我们可以将文件导入到main.ts中通过app.directive全局注册:

import { createApp } from "vue";

import vClickOutside from "./v-click-outside";

const app = createApp(App);
// 注意这玩意一定要写在app.mount("#app");之前.
app.directive('click-outside', vClickOutside);

app.mount("#app");