用vue3自定义指令写一个按钮点击小优化

794 阅读4分钟

背景

例如我们有个功能,点击保存请求接口操作。

<template>
  <el-form :model="form" label-width="120px">
    <el-form-item label="Activity name">
      <el-input v-model="form.name" />
    </el-form-item>
    <el-form-item label="Activity form">
      <el-input v-model="form.desc" type="textarea" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="onSubmit">确认</el-button>
      <el-button>取消</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
import { reactive } from 'vue';

const form = reactive({
  name: '',
  desc: '',
});

const onSubmit = () => {
  console.log('submit!');
};
</script>

如果连续点击保存按钮,会一直请求接口,可能会造成服务器压力,如果点击过快接口有可能会报错。

思路

首先我们想一下该怎么解决这个问题?

  1. 使用防抖函数,点击调用事件处理
  2. 控制点击行为,某个时间段不能点击
  3. 接口加锁控制请求
  4. 写个公用的组件,点击事件控制

思路1和思路3每个点击事件都要做处理,处理起来有点麻烦, 相对思路2和思路4来说,控制点击行为还是比较简单点,而且方便。

实现

这里我们用自定义指令实现

知识点

工欲善其事,必先利其器

首先我们看下自定义指令的一些知识点

指令钩子#

一个指令的定义对象可以提供几种钩子函数 (都是可选的):

const myDirective = {
  // 在绑定元素的 attribute 前
  // 或事件监听器应用前调用
  created(el, binding, vnode, prevVnode) {
    // 下面会介绍各个参数的细节
  },
  // 在元素被插入到 DOM 前调用
  beforeMount(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都挂载完成后调用
  mounted(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {},
  // 在绑定元素的父组件
  // 及他自己的所有子节点都更新后调用
  updated(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载前调用
  beforeUnmount(el, binding, vnode, prevVnode) {},
  // 绑定元素的父组件卸载后调用
  unmounted(el, binding, vnode, prevVnode) {}
}

钩子参数#

指令的钩子会传递以下几种参数:

  • el:指令绑定到的元素。这可以用于直接操作 DOM。

  • binding:一个对象,包含以下属性。

    • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
    • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
    • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
    • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
    • instance:使用该指令的组件实例。
    • dir:指令的定义对象。
  • vnode:代表绑定元素的底层 VNode。

  • prevNode:之前的渲染中代表指令所绑定元素的 VNode。仅在 beforeUpdate 和 updated 钩子中可用。

实战

1. 创建指令

我们创建一个button指令, 监听点击事件,点击设置按钮不可点击,800ms后恢复,防止重复点击。

这里设置V开头,是方便使用<script setup>

// directives.js
export const VButton = {
  mounted: (el, binding) => {
    el.handler = function () {
      el.classList.add('is-disabled');
      el.disabled = true;
      setTimeout(() => {
        el.classList.remove('is-disabled');
        el.disabled = false;
      }, 800);
    };
    el.addEventListener('click', el.handler);
  },
  unmounted: (el) => {
    el.removeEventListener('click', el.handler);
  },
};

2. 组件中使用

<template>
  <el-form :model="form" label-width="120px">
    <el-form-item label="Activity name">
      <el-input v-model="form.name" />
    </el-form-item>
    <el-form-item label="Activity form">
      <el-input v-model="form.desc" type="textarea" />
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="onSubmit" v-button>确认</el-button>
      <el-button>取消</el-button>
    </el-form-item>
  </el-form>
</template>

<script setup>
import { reactive } from 'vue';
import { VButton } from './directives'

const form = reactive({
  name: '',
  desc: '',
});

const onSubmit = () => {
  console.log('submit!');
};

3. 全局使用

// main.js
import { createApp } from 'vue';
import App from './App.vue';
import { VButton } from '@/utils/directives'

const app = createApp(App);
app.directive('button', VButton);

现在再使用按钮点击,就不会连续点击连续请求接口的情况了,是不是很简单。

优化

功能实现了,下一步就是优化功能和代码。

首先我们先来想一下:

  1. 是不是可以动态设置防抖时间?
  2. 是不是可以点击一次加锁,不能点击,重新刷新页面才能点击?once
  3. 其他...

这里只是给你一个思路,具体业务具体安排。

我们来实现问题1和问题2,该怎么传参是我们所需要的。

binding:一个对象,包含以下属性。

  • value:传递给指令的值。例如在 v-my-directive="1 + 1" 中,值是 2
  • oldValue:之前的值,仅在 beforeUpdate 和 updated 中可用。无论值是否更改,它都可用。
  • arg:传递给指令的参数 (如果有的话)。例如在 v-my-directive:foo 中,参数是 "foo"
  • modifiers:一个包含修饰符的对象 (如果有的话)。例如在 v-my-directive.foo.bar 中,修饰符对象是 { foo: true, bar: true }
  • instance:使用该指令的组件实例。
  • dir:指令的定义对象。

再来看一下,这里有个参数,我们可以通过binding获取参数.

  • 我们先通过value来实现传参
<template>
   <el-button type="primary" @click="onSubmit" v-button="{ delay:1000 }">
   确认
   </el-button>
</template>

打印结果看下,可以看到value对象中delay,这就好办了。

image.png

我们修改下directives.js

// directives.js
export const VButton = {
  mounted: (el, binding) => {
    el.handler = function () {
      const { delay = 800 } = binding.value || {};
      el.classList.add('is-disabled');
      el.disabled = true;
      setTimeout(() => {
        el.classList.remove('is-disabled');
        el.disabled = false;
      }, delay);
    };
    el.addEventListener('click', el.handler);
  },
  unmounted: (el) => {
    el.removeEventListener('click', el.handler);
  },
};

  • 然后我们使用修饰符实现问题2,只能点击一次once
<template>
   <el-button type="primary" @click="onSubmit" v-button.once="{ delay:1000 }">
   确认
   </el-button>
</template>

image.png

我们再次修改下directives.js

// directives.js
export const VButton = {
  mounted: (el, binding) => {
    el.handler = function () {
      const { delay = 800 } = binding.value || {};
      el.classList.add('is-disabled');
      el.disabled = true;
      const { once } = binding.modifiers;
      if (once) return;
      setTimeout(() => {
        el.classList.remove('is-disabled');
        el.disabled = false;
      }, delay);
    };
    el.addEventListener('click', el.handler);
  },
  unmounted: (el) => {
    el.removeEventListener('click', el.handler);
  },
};

是不是感觉很简单,这里只是简单的做个扩展,其他的自定义指令类似。