重复点击-注册全局指令

529 阅读6分钟

使用场景

  1. 提交表单的时候,只有在表单成功提交之后,才能下一次提交,处于网络请求中的时候,不能够提交。
  2. 付款的时候,只能够付一次,同样处于网络请求中,不能够提交等等

优点(也算是吧):

  1. 相对于使用防抖和节流的操作,防抖和节流会受到网络请求的快慢影响,如果网络请求过慢,而且两次点击的时间超过防抖和节流预设的时间,还会触发多次的。本方式使用捕捉promise中resolve和reject的方式,更加精确在什么时候可以触发禁止点击和放开点击
  2. 相对于使用变量控制,不需要额外的声明变量,只需要使用解构过来的resolve和reject控制即可。
  3. 因为禁用和非禁用使用代码精确控制,所以适用的场景应该很多。目前在网络请求的案例上,问题都能够解决。

存在的缺点:

  1. 可能使用解构出来的东西,你并不习惯

  2. 如果遇到了提交表单的情况,且表单没验证通过,不能进行提交的话,也要需要使用reject,表示下次继续可以点击。

    另外表单验证只是另外的一种,确切的说,只要用户进行了点击,那么那一瞬间或者说是只要没使用reject就是不能够进行下一次点击的。如果想要继续可以点击,需要使用reject。**所以在使用该指令的时候,注意reject的时机!!!

参数解释:

参数:params , options

  1. params:传入的参数,任意类型

    <div class="submit" v-reclick:applyRecruit=“{parans:111}”>申请加入</div>
    <div class="submit" v-reclick:applyRecruit=“{parans:'aaa'}”>申请加入</div>
    <div class="submit" v-reclick:applyRecruit=“{parans:{name:'tom',age:16}}”>申请加入</div>
    
  2. options:预留配置项,暂时没有用,不用传值

使用方法

  1. 将下面的函数放入到utils公共方法中,或者是放到一个js文件中

    // h5-better-examples/utils/directive.js
    const handleClick = async (e, binding, vnode) => {
        let target = binding.value || {};
        let {params = null, options = {}} = target;
    
        let flag = Number(e.target.getAttribute('data-click'));
        if (flag) {
            e.preventDefault();
            return;
        }
        let fn = binding.arg;
        if (Object.prototype.toString.call(options) !== '[object Object]') {
            throw new Error('options必须是一个对象');
        }
        if (typeof vnode.context[fn] !== 'function') {
            throw new Error('v-clickounce绑定的必须是一个函数');
        }
        // 标识该元素进行点击过了
        e.target.setAttribute('data-click', 1);
    
        try {
            await new Promise((resolve, reject) => vnode.context[fn]({resolve, reject, e, params}));
        } catch (err) {
            console.log(err);
            // 因出现了catch的情况,说明本次异步请求出错了,所以应该能够再次进行点击
            e.target.setAttribute('data-click', 0);
        }
    };
    
    /**
     * 参数是由vue2的注册指令时候传过来的参数,如有需要,vue文档中查看属性对应的值https://cn.vuejs.org/v2/guide/custom-directive.html
     * el:点击的节点
     * binding
     * vnode:Vue 编译生成的虚拟节点。
     */
    export const setReclickOption = {
        inserted: function(el, binding, vnode) {
            // 初始化,将data-click置为0,用于标识该元素没有点击过
            el.setAttribute('data-click', 0);
            el.addEventListener('click', e => handleClick(e, binding, vnode));
        },
        unbind(el) {
            el.removeEventListener('click', handleClick);
        }
    };
    
    
  2. main.js中引入上面的这个setReclickOption,并且注册。然后就可以在vue文件中使用了

    import {setReclickOption} from './utils/directive';
    Vue.directive('clickonce', setReclickOption);
    
  3. 使用

    <div class="submit" v-reclick:applyRecruit=“{parans:{name:'tom',age:16}}”>申请加入</div>
    
  4. 解构需要使用的参数

    e:触发的事件 , 和click中触发的e相同。 reject/resolve:控制下次是否可以点击,实际上只需要resolve就可以了 。 params:传递的参数

     clickResolve({resolve, reject, e, params}) {
        let resolveURL = ''
         this.$ax.get(resolveURL)
             .then(res => {
                console.log(e, params);
           }).catch(err => {
               console.log(err);
               reject();
       });
    },
    

实现思路

实现的思路非常简单,也就是在注册指令的时候对使用v-reclick事件的dom元素进行监听,监听他的click点击事件。并且在该dom上添加data-click的属性,初始设置为0,标记可以进行点击(data-click标记0可以点击,1不可以点击)。

  1. 注册的时候,初始化data-click设置为0,并且监听这个dom的click事件

    export const setReclickOption = {
        inserted: function(el, binding, vnode) {
            // 初始化,将data-click置为0,用于标识该元素没有点击过
            el.setAttribute('data-click', 0);
            el.addEventListener('click', e => handleClick(e, binding, vnode));
        },
        unbind(el) {
            el.removeEventListener('click', handleClick);
        }
    };
    
  2. 触发click事件,会先判断data-click是0还是1 , 是0则可以触发点击事件触发的函数

    const handleClick = async(e ,  binding) =>{
        // ...
        let flag = Number(e.target.getAttribute('data-click'));
        if (flag) {
            e.preventDefault();
            return;
        }
        // ...
    }
    
  3. 触发之前,判断传递的参数是否正确,并且将状态立即设置为1

    const handleClick = async(e ,  binding) =>{
        // ...
        
        if (Object.prototype.toString.call(options) !== '[object Object]') {
            throw new Error('options必须是一个对象')
    
        }
        if (typeof binding.instance[fn] !== 'function') {
            throw new Error('v-clickounce绑定的必须是一个函数')
        }
        // 标识该元素进行点击过了
        e.target.setAttribute('data-click', 1);
        
        // ...
    }
    
    
  4. 加下来触发函数,因为改函数使用promise封装,所以函数执行过程中在try...catch中可控,根据promise中的resolve/reject。如果是resolve则data-click则为1,不可进行点击,如果是reject,则可以继续点击

    const handleClick = async(e ,  binding) =>{
        // ...
        
    	try {
             await new Promise((resolve, reject) => binding.instance[fn]({resolve, reject, e, params}));
        } catch (err) {
            console.log('点击操作失败', err);
    		 e.target.setAttribute('data-click', 0);
        }
        
        // ...
    }
    

指令注册代码(vue3中)

const handleClick = async(e ,  binding) =>{
    let target = binding.value || {}
    let { params = null, options = {} } = target
    let fn = binding.arg;
    
    let flag = Number(e.target.getAttribute('data-click'));
    if (flag) {
        e.preventDefault();
        return;
    }
    
    if (Object.prototype.toString.call(options) !== '[object Object]') {
        throw new Error('options必须是一个对象')

    }
    if (typeof binding.instance[fn] !== 'function') {
        throw new Error('v-clickounce绑定的必须是一个函数')
    }
    // 标识该元素进行点击过了
    e.target.setAttribute('data-click', 1);

    try {
         await new Promise((resolve, reject) => binding.instance[fn]({resolve, reject, e, params}));
    } catch (err) {
        console.log('点击操作失败', err);
		 e.target.setAttribute('data-click', 0);
    }
}

// 注册指令
export const setReclickOption ={
    mounted: function (el, binding) {
        el.setAttribute('data-click', 0);
        el.addEventListener('click', e=> handleClick(e ,  binding))

    },
    unmounted(el){
        el.removeEventListener('click' ,handleClick )
    }
}

指令注册代码(vue2中)

vue2中没有bind的第二个参数中没有instance属性(组件的实例),需要到第三个参数中去拿组件虚拟dom中的方法。

然后是注册指令时的钩子函数不同,vue2中的是bindunbind,在vue3中使用的是mountedunmounted

// h5-better-examples/utils/directive.js
const handleClick = async (e, binding, vnode) => {
    let target = binding.value || {};
    let {params = null, options = {}} = target;

    let flag = Number(e.target.getAttribute('data-click'));
    if (flag) {
        e.preventDefault();
        return;
    }
    let fn = binding.arg;
    if (Object.prototype.toString.call(options) !== '[object Object]') {
        throw new Error('options必须是一个对象');
    }
    if (typeof vnode.context[fn] !== 'function') {
        throw new Error('v-clickounce绑定的必须是一个函数');
    }
    // 标识该元素进行点击过了
    e.target.setAttribute('data-click', 1);

    try {
        await new Promise((resolve, reject) => vnode.context[fn]({resolve, reject, e, params}));
    } catch (err) {
        console.log(err);
        // 因出现了catch的情况,说明本次异步请求出错了,所以应该能够再次进行点击
        e.target.setAttribute('data-click', 0);
    }
};

/**
 * 参数是由vue2的注册指令时候传过来的参数,如有需要,vue文档中查看属性对应的值https://cn.vuejs.org/v2/guide/custom-directive.html
 * el:点击的节点
 * binding
 * vnode:Vue 编译生成的虚拟节点。
 */
export const setReclickOption = {
    inserted: function(el, binding, vnode) {
        // 初始化,将data-click置为0,用于标识该元素没有点击过
        el.setAttribute('data-click', 0);
        el.addEventListener('click', e => handleClick(e, binding, vnode));
    },
    unbind(el) {
        el.removeEventListener('click', handleClick);
    }
};

bingding参数截图

vnode参数截图