vue自定义指令 防止连点

针对按钮以及非按钮元素防止连续点击的处理方法

使用方法

  • 可参考文档v-focus例子
  • 对普通 DOM 元素进行底层操作的时候可用到自定义指令,它们可以用于操纵DOM、响应事件和修改元素样式等操作
  • 一般在nuxt脚手架的配置文件nuxt.config.js中配置plugins,在plugins/plugins.js中引入import '@/utils/directives';utils/directives.js中使用Vue.directive设置全局自定义指令
  • 创建基本步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// directive-name 是自定义指令的名称
Vue.directive('directive-name', {
// 对象中的各个钩子函数(bind、inserted、update和unbind)用于定义指令的行为
bind: function(el, binding, vnode) {
// 绑定时的初始化逻辑
},
inserted: function(el, binding, vnode) {
// 元素插入到父节点时的逻辑
},
update: function(el, binding, vnode, oldVnode) {
// 组件更新时的逻辑
},
unbind: function(el, binding, vnode) {
// 解绑时的清理逻辑
}
});
  • 应用自定义指令:
1
2
3
4
<div v-directive-name></div>

<!-- 可以通过指令修饰符(Directive Modifiers)和指令参数(Directive Arguments)来进一步定制指令的行为 -->
<div v-directive-name.modifier="value"></div>

工作场景

  • 各函数区别可参考文档-自定义指令-钩子函数
  • 期望实现效果:点击按钮要立即执行点击事件
  • 处理方式:直接从样式上做限制简单,按钮控制disabled 属性,非按钮则控制CSS3 pointer-events 属性
    • v-preventReClick只是防止按钮被连续点击,只控制disable3s,不是防抖
    • 比直接对按钮的事件函数用防抖效果要好,直接用防抖则还得额外在页面处理按钮的loading或者disabled展示给用户看?
    • 如果做防抖,则还要考虑改写原本的点击事件,比如@click='func'改为v-xxx-click='func',很麻烦,干脆直接改样式,如果有需要防抖的还是对事件处理函数使用防抖处理
  • 注意:
    • 使用了addEventListener的,需要加一个 unbind 钩子函数在指令解绑时清除事件监听器,可确保在组件销毁或指令解绑时,移除按钮的事件监听器,避免潜在的内存泄漏问题

防止按钮重复点击(不是防抖)

  • 为防止按钮重复点击,增加v-preventReClick指令
  • 参考
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建全局指令 用于防止按钮重复点击
Vue.directive("preventReClick", {
inserted: function(el, binding) {
el.handler = () => {
if (!el.disabled) {
el.disabled = true;
el.classList.add('is-disabled')
setTimeout(() => {
el.disabled = false;
el.classList.remove('is-disabled')
}, binding.value || 3000);
}
}
el.addEventListener('click', el.handler)
},
unbind(el) {
// console.log('preventReClick', el)
el.removeEventListener('click', el.handler)
el = null
},
});
1
2
<!-- 使用v-preventReClick -->
<el-button type="primary" size="mini" @click="handle().optionhandle()" v-preventReClick>查询</el-button>
  • 出现问题:对于button可以使用disabled属性让其不触发,但有些按钮是用 div / span 等这种实现的,disabled属性并不能阻止div的onclick事件,此时可使用v-pointerReClick
  • 解决:设置样式 pointer-events: none ,该样式会将元素设置为不接收鼠标事件,包括点击事件

对非按钮元素点击事件加防抖命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 用于防止按钮重复点击 可作用于div/span
Vue.directive('pointerReClick', {
inserted(el, binding) {
el.handler = () => {
el.style.pointerEvents = 'none'
setTimeout(() => {
el.style.pointerEvents = 'auto'
}, binding.value || 1000)
}
el.addEventListener('click', el.handler)
},
unbind(el) {
el.removeEventListener('click', el.handler)
el = null
},
})
1
2
<!-- 使用v-pointerReClick -->
<span class="pointer" v-pointerReClick>文字</span>
  • 出现问题:
    • 由于设置为pointer-events:none后,相当于该元素已经不存在了,那么这种情况的点击会不会穿透导致点击到它的外层呢?
    • 结果是肯定的,设置pointer-events:none后,点击了子节点是无效的,但同时相当于点击了其父节点
  • 解决方法:设置修饰符阻止冒泡?行不通,只能在父元素中查子元素pointerEvents未none时return出点击函数,不好做自定义指令,直接用封装的节流函数处理childClick比较方便。如果实在要做,可在自定义指令中使用节流(注意:如果做防抖节流的自定义指令,则还要改写原本的点击事件,比如**@click='func'改为v-xxx-click='func'**,再加上样式控制)
  • 复杂的解决:只能在父元素中查子元素pointerEvents未none时return出点击函数,不好做自定义指令
1
2
3
4
5
6
7
8
9
parentClick() {
if (window.getComputedStyle(this.$refs.child).pointerEvents === 'none') {
return;
}
console.log('父元素被点击');
},
childClick(event) {
console.log('子元素被点击');
}
  • 可以放弃自定义指令,直接用封装的节流函数处理childClick,样式另外写
1
2
3
childClick: _.throttle(function(){
console.log('子元素被点击')
}, 3000),
  • 最终解决:自定义指令中使用节流(注意:如果做防抖节流的自定义指令,则还要改写原本的点击事件,比如@click='func'改为v-xxx-click='func',再加上样式控制)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Vue.directive("throttle-click", {
bind(el, binding) {
let timer = null;
const delay = parseInt(binding.arg) || 3000; // 获取节流的延迟时间

el.handler = (event) => {
// 防止冒泡,不然会触发父元素点击事件
event.stopPropagation();
if (!timer) {
// 如果定时器不存在,则执行指令绑定的处理函数
binding.value();
// 样式控制
el.classList.remove('pointer')
timer = setTimeout(() => {
timer = null;
el.classList.add('pointer')
}, delay);
}
}

el.addEventListener('click', el.handler);
},
unbind: function(el, binding, vnode) {
el.removeEventListener('click', el.handler)
}
});
1
2
3
.pointer {
cursor: pointer;
}
1
2
3
<div @click="parentClick">
<span v-throttle-click="childClick" class="pointer" ref="child">123</span>
</div>
  • 用lodash中节流则event不太好传:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 能用lodash中节流
import _ from 'lodash';

Vue.directive("throttle-click", {
bind(el, binding) {
let timer = null;
const delay = parseInt(binding.arg) || 3000; // 获取节流的延迟时间

// 节流函数
el.throttleClick = _.throttle(function() {
binding.value();
el.classList.add('pointer')
}, delay)

el.handler = (event) => {
// 样式控制
el.classList.remove('pointer')
// 防止冒泡,不然会触发父元素点击事件
event.stopPropagation();
el.throttleClick()
}
el.addEventListener('click', el.handler);
},
unbind: function(el, binding, vnode) {
el.removeEventListener('click', el.handler)
}
});

binding.value()调用组件函数

  • 在 Vue 的自定义指令中,通过 binding.value() 可以直接调用指令绑定的处理函数,而不需要使用 this 关键字
  • 在指令的 bind 钩子函数中,binding.value 表示指令绑定的值,通常是一个函数。通过调用 binding.value(),就可以直接触发该函数。
  • 这是因为在 Vue 的指令系统中,binding.value 属性被设置为绑定指令时提供的值。当你在模板中使用指令时,可以将一个函数作为指令的值传递,并在自定义指令中通过 binding.value() 调用该函数。
  • 需要注意的是,通过 binding.value() 调用的函数,其上下文(this)可能会发生变化。如果需要在函数内部使用特定的上下文,可以使用 Function.prototype.bind() 方法来绑定函数的执行上下文
1
binding.value.bind(context)(); // context 是你希望绑定的执行上下文

区分修饰符prevent和stop

  • prevent是阻止默认事件
  • stop是阻止事件传播

,