【前端】如何优雅的解决按钮“重复点击”问题

您所在的位置:网站首页 elementui按钮防止重复点击 【前端】如何优雅的解决按钮“重复点击”问题

【前端】如何优雅的解决按钮“重复点击”问题

2024-03-28 18:33| 来源: 网络整理| 查看: 265

如何优雅的解决按钮“重复点击”问题 一、背景:二、解决方案:html部分按钮代码:方案一:按钮添加二次确认功能(利用ant design 的 `Modal组件` )方案二:利用 async await方案三:按钮上锁方案四:防抖(在一定时间内,动作只会执行一次)方案五:节流方案六:CSS动画精准控制简要说明: 总结:

一、背景:

在项目中会有很多按钮点击调取接口的需求(比如:提交操作),很多业务用户会不自觉点两次或多次,就导致按钮click方法会多次触发,导致最后保存了好几条一模一样的数据,就会出现脏数据的情况

二、解决方案: html部分按钮代码: 提交 方案一:按钮添加二次确认功能(利用ant design 的 Modal组件 ) //提交 saveSubmit() { const this_ = this // 提交前的数据处理 Modal.confirm({ title: '确认进行提交操作?', // content:可以不写 content: '操作成功将生成新数据', // 确认按钮:在这里调接口保存数据 okText: '确认', cancelText: '取消', zIndex: 10000, onOk() { Api.save(data).then((res) => { if (!res.success) { this.$message.error(res.message) return } Util.closeWindow(this_, 'ok', '1') }) }, // 点击取消没有特殊操作可以不写onCancel() onCancel() { console.log('Cancel') }, }) },

但是,如果产品和业务觉得二次确认很繁琐的话,我们就只能使用下面几种方案开解决问题了

方案二:利用 async await saveSubmit: async () => { const res = await Api.save(data) if (!res.success) { this.$message.error(res.message) return } }, 方案三:按钮上锁 saveSubmit(func, manual = false) { let lock = false return function (data) { if (lock) return lock = true // 假设使用axios发送请求 axios.post('urlxxx', postParams).then( // 表单提交成功 ).catch(error => { // 表单提交出错 console.log(error) }).finally(() => { // 不管成功失败 都解锁 lock = false }) } }

由于是button按钮,所以可以使用setAttribute('disabled', xxx)和 removeAttribute('disabled')来代替lock标记,但是要注意我们必须请求接口完成之后先关闭页面或者弹框,再去解除按钮禁用,否则会出现页面还没关闭,按钮已解除禁用,导致用户仍然可以快速点击到。 在项目里可能不只是按钮标签有这样的需求,所以还是推荐用上锁去控制,如果使用的场景比较多的话,这样会有很多重复的lock标记逻辑出现在代码各个地方,所以我们在这里封装一下(提供两种解锁方式:手动解锁 和 自动解锁):

// func: 将 **点击回调函数func** 作为参数传递给ignoreMultiClick // manual: 用于手动解锁 function ignoreMultiClick(func, manual = false) { let lock = false return function (...args) { if (lock) return lock = true // done 函数,用于手动解锁 let done = () => (lock = false) // manual:若该参数为true,则点击事件触发时会给原始的点击回调func传递一个参数done,done是一个函数,调用它可以直接解锁 if (manual) return func.call(this, ...args, done) // 可以使原监听函数func返回一个promise,在该promise决议后自动执行解锁操作。因为Promise管理回调函数非常方便,并且像axios这样非常常用的请求库返回值本身也是一个promise,所以默认情况使用这种方式。 let promise = func.call(this, ...args) // 当然返回promise并不是必须的,有时候我们在发请求前会进行一些验证,验证没通过则直接return,此时装饰器函数也能正常处理,因为使用Promise.resolve包裹了一下promise: Promise.resolve(promise).finally(done)。 Promise.resolve(promise).finally(done) return promise } }

如何使用这个实例呢:

自动解锁: // 利用Promise自动解锁 let saveSubmit = ignoreMultiClick(function (data) { if (!checkForm()) return // 假设有一些检测表单的操作,检查不通过则直接返回 // 返回promise return axios.post('urlxxx', data).then( // 表单提交成功 ).catch(error => { // 表单提交出错 console.log(error) }) }) 手动解锁: let saveSubmit = ignoreMultiClick(function (data, done) { if (!checkForm()) return done() // 表单验证不通过解锁 axios.post('urlxxx', data).then( // 表单提交成功 ).catch(error => { // 表单提交出错 console.log(error) }).finally(() => done()) // 请求结束解锁 }, true)

参考: 普通场景下还是自动解锁比较简单,因为可能有多个条件分支,手动解锁需要在每一个返回的地方都调用done

方案四:防抖(在一定时间内,动作只会执行一次)

防抖的思想是:只有在用户操作结束后一段时间内没有再次操作,才执行该操作,以此来避免执行过于频繁的操作。在实现上,可以通过 SetTimeout 方法来延迟执行操作,具体实现如下:

saveSubmit() { // 维护一个 timer,用来记录当前执行函数状态 let timer = null; // 注意清除定时器 clearTimeout(timer) timer= do(() => { Api.save(data).then((res) => { if (!res.success) { this.$message.error(res.message) return } }) }, 1000); }

同样由于使用量较大,我们可以封装一下(vue可以封装为自定义指令):

创建debounce.js export default { install(Vue) { Vue.directive("debounce", { // bind 指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置,比如我们可以设置样式 bind(el) { // console.log(el) }, // 被绑定的元素插入到父节点时 inserted(el, binding) { el.addEventListener('click', function () { let time = binding.value.time; let methods = binding.value.methods; clearTimeout(el.timeId) el.timeId = setTimeout(() => { methods() }, time); }) } }) } } 在vue文件或者直接在main.js中引入该函数 import debounce from "./debounce" 按钮点击使用该指令 v-debounce // html部分 提交 // javascript部分 saveSubmit(){ Api.save(data).then((res) => { if (!res.success) { this.$message.error(res.message) return } }) }

这样就可以避免用户快速的多次点击按钮,等待一段时间后才会真正执行相应操作。

方案五:节流

节流的思想是:无论操作多频繁,只有在一段时间内执行一次操作,以此来避免重复执行操作。在实现上,可以通过 SetTimeout 及时间戳进行控制,具体实现如下:

function throttle(func, wait) { let previous = 0; return function() { const context = this; const args = arguments; const now = Date.now(); if (now - previous > wait) { func.apply(context, args); previous = now; } } } // 调用方法 const handleClick = ()=> { console.log('click'); } const throttledClick = throttle(handleClick, 300); myButton.addEventListener('click', throttledClick);

这样就可以保证在一段时间内只执行一次操作,避免出现重复执行的情况。

方案六:CSS动画精准控制 先定义一个关于pointer-events的动画,就叫做 throttle,代码如下: @keyframes throttle { from { color: red; pointer-events: none; } to { color: green; pointer-events: all; } }

很简单,就是从禁用到可点击的变化。

接下来,将这个动画绑定在按钮上,这里为了方便测试,将动画设置成了2s button{ animation: throttle 2s step-end forwards; } button:active{ animation: none; }

注意,这里动画的缓动函数设置成了阶梯曲线step-end,它可以很方便的控制pointer-events的变化时间点 如下示意,pointer-events在0~2秒内的值都是none,一旦到达2秒,就立刻变成了all,由于是forwards,会一直保持all的状态 现在如果文字是red,表示是禁用态,只有是green,才表示可以被点击,非常清晰明了

简要说明:

函数节流是一个非常常见的优化方式,可以有效避免函数过于频繁的执行 CSS 的实现思路和 JS 不同,重点在于在于找到和该场景相关联的属性 CSS 实现“节流” 其实就是 控制一个动画的精准控制,假设有一个动画控制按钮从禁用->可点击的变化,每次点击时让这个动画重新执行一遍,在执行的过程中,一直处于禁用状态,这样就达到了“节流”的效果 还可以通过 transition 的回调函数动态设置按钮禁用态,这种实现的好处在于禁用逻辑和业务逻辑是完全解耦的 不过,这种实现方式还是比较有局限的,仅限于点击行为,像很多时候,节流可能会用在滚动事件或者键盘事件上,像这些场景就用传统方式实现就行了。

总结:

总体来说,防抖和节流都是非常实用的优化手段,具体的使用场景和优缺点需要根据具体情况进行选择,以实现更好的用户体验。大家根据自己项目的实际需求去挑选方案,上面的都不是重点,重点是:欢迎大佬们评论指导!!!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3