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

一、背景:

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

二、解决方案:

html部分按钮代码:

<a-button type="primary" @click="saveSubmit" >提交</a-button>

方案一:按钮添加二次确认功能(利用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
  }
}

如何使用这个实例呢:

  1. 自动解锁:
// 利用Promise自动解锁
let saveSubmit = ignoreMultiClick(function (data) {
  if (!checkForm()) return // 假设有一些检测表单的操作,检查不通过则直接返回
  // 返回promise
  return axios.post('urlxxx', data).then(
    // 表单提交成功
  ).catch(error => {
    // 表单提交出错
    console.log(error)
  })
})
  1. 手动解锁:
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部分
<a-button v-debounce="{ time: 1000, methods: saveSubmit }">提交</a-button>
// 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动画精准控制

  1. 先定义一个关于pointer-events的动画,就叫做 throttle,代码如下:
@keyframes throttle {
  from {
    color: red;
    pointer-events: none;
  }
  to {
    color: green;
    pointer-events: all;
  }
}

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

  1. 接下来,将这个动画绑定在按钮上,这里为了方便测试,将动画设置成了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 的回调函数动态设置按钮禁用态,这种实现的好处在于禁用逻辑和业务逻辑是完全解耦的
不过,这种实现方式还是比较有局限的,仅限于点击行为,像很多时候,节流可能会用在滚动事件或者键盘事件上,像这些场景就用传统方式实现就行了。

总结:

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

  • 10
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值