Vue自定义指令防抖

722 阅读1分钟

vue项目中写了好多防抖函数以后, 想利用自定义指令实现一下。

开始想法是实现一个自定义的click事件:

// main.js

import Directives from '@/directives'
Vue.use(Directives)
// directives/index.js

import debounceClick from './debounceClick'

const directives = {
  debounceClick,
}

export default {
  install(Vue) {
    Object.keys(directives).forEach(key => {
      Vue.directive(key, directives[key])
    })
  },
}
// directives/debounceClick

import debounce from 'lodash/debounce'

export default {
  // 指令第一次绑定元素时使用,在这里进行初始化
  bind: (el, { arg, value }) => {
    if (arg && !/^\d+$/.test(arg)) {
      throw new Error('时间填写不对')
    }
    const time = Number(arg) || 300
    const debounceClickFn = debounce(value, time, {
      leading: true,
      trailing: false,
    })
    el._debounceClick = debounceClickFn
    el.addEventListener('click', el._debounceClick, true)
  },
  unbind: el => {
    el.removeEventListener('click', el._debounceClick)
    delete el._debounceClick
  },
}

使用方法就像下面这样:

<div v-debounce-click="onClickName">点我呀</div>
<div v-debounce-click:5000="onClickName">这是一个5秒的防抖</div>

本来像上面那样就解决了需求。 但是, 本着学习的态度, 网上搜一些关于自定义指令防抖的文章, 发现了不一样的思路。

我的思路就是完全自定义一个自己的防抖指令函数, 来替代原有的@click事件, 而找了几篇文章之后发现还有别的思路:@click正常写, 另外添加一些自定义指令做控制, 类似下面这样:

<div @click="onClickName" v-debounce>点我呀</div>

而实现原理是利用stopImmediatePropagation来屏蔽/放开元素加载本身绑定的click事件。

以下摘自【实战】Vue自定义防抖指令 - 掘金 (juejin.cn), 博主写的是throttle

Vue.directive('throttle', {
  bind: (el, binding) => {
    let throttleTime = binding.value; // 防抖时间
    if (!throttleTime) { // 用户若不设置防抖时间,则默认2s
      throttleTime = 2000;
    }
    let cbFun;
    el.addEventListener('click', event => {
      if (!cbFun) { // 第一次执行
        cbFun = setTimeout(() => {
          cbFun = null;
        }, throttleTime);
      } else {
        event && event.stopImmediatePropagation();
      }
    }, true);
  },
});
<button @click="sayHello" v-throttle>提交</button>

不过我每次自己写 节流/防抖 都写不好, 生怕漏了什么边界条件, 所以稍稍改了一下:

// directives/debounce

import debounce from 'lodash/debounce'

export default {
  // 指令第一次绑定元素时使用,在这里进行初始化
  bind: (el, { arg, value = {} }) => {
    if (!arg) {
      throw new Error('请填写事件名称')
    }
    if (value.time && !/^\d+$/.test(value.time)) {
      throw new Error('时间填写不对')
    }
    const time = Number(value.time) || 300
    let flag = true // 是否调用触发的事件
    const debounceClickFn = debounce(() => (flag = true), time, {
      leading: true,
      trailing: false,
      ...value,
    })
    el._arg = event => {
      flag = false
      debounceClickFn()
      if (!flag) {
        event?.stopImmediatePropagation()
      }
    }
    el.addEventListener(arg, el._arg, true)
  },
  unbind: (el, { arg }) => {
    el.removeEventListener(arg, el._arg)
    delete el._arg
  },
}

使用方式如下:

<div @click="onClickName" v-debounce:click>点我呀</div>
<div @click="onClickName" v-debounce:click="{time: 5000}">这是一个5秒的防抖</div>

由于思路打开以后, 我觉得可以把这个指令给扩充一下了。两个自定义指令, 一个v-throttle一个v-debounce。 传递给指令的参数arg字段, 也就是:后面的参数, 可以携带click或者别的一些事件参数, 就更加通用。 另外自定义指令的修饰符对象modifiers, 可以拿来用作一些设置的集成, 这个根据项目需求可以自己设置。