按钮不是你想点,想点就能点

187 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

前言

通常页面上获取验证码的按钮,每次点击过后,都会在几十秒内禁用,这是一个比较常见的需求。最近就碰到了这种需求,不同的点在于,需要处理的是一大推按钮。
项目需求是在一个表格中,每行数据都有几个按钮用于发起请求更改数据,那么肯定不能让用户连续点击同一个按钮,连续发起请求,不然服务器会受不了,引发各种令人头痛的问题。
但问题是页面是接手别人的,而且表格中按钮多,功能多,代码复杂,不适合大动干戈进行处理。

效果

思路分析

如果原有的代码不适合改动,那么能做就是在原有的代码上加一层处理逻辑。众所周知,浏览器的事件如果没有特意去处理,是会向上冒泡的,在所有按钮的共同父元素上挂载一个监听点击的事件,就可以监听到所有子元素的点击事件,也就是事件委托。
然后,可以从点击事件中获取到target属性,该属性指向被点击的元素。既然获取到了被点击的元素,那如果在那些不可连续点击的按钮上设置一些标识,是不是就可以区分出来,然后对这些按钮进行特殊处理即可。

代码

首先,在不可连续点击的按钮上,通过data-*属性,设置标识interval和禁用时间time

<button
  id="interval-btn"
  data-interval="true"
  data-time="50"
> 间隔按钮1 </button>

接着,在所有按钮的共同父级元素上挂载一个监听事件,对被点击的元素进行区分,挑选出不可连续点击的按钮,进行特殊处理,此处示例挂载在document.body:

// 当前禁用的按钮列表
let intervalList = []
//  requestAnimationFrame 返回句柄
let intervalBtnTimer = 0
// 监听点击事件
document.body.addEventListener('click', (event) => {
   // 获取被点击元素
  const target = event.target
  // 判断被点击元素是否需要进行禁用处理
  if (target.dataset.interval !== 'true') {
    return
  }
  const time = target.dataset.time
  // 创建当前禁用的按钮列表的元素
  const intervalItem = {
    target,
    time: (time ? +time : 50) * 1000
  }
  intervalList.push(intervalItem)
  // 更改按钮样式,将其处理成不可点击状态
  target.dataset.tip = Math.ceil(intervalItem.time / 1000) + '秒后可点击'
  target.disabled = true
  target.classList.add('showInterval')
  // intervalBtnTimer是requestAnimationFrame返回的句柄,不为零,如果为零,代表没有当前没有执行计时;如果没有执行requestAnimationFrame,开始执行
  intervalBtnTimer === 0 && (intervalBtnTimer = requestAnimationFrame(execIntervalBtnCheck))
})

上面的代码中使用requestAnimationFrame来进行计时操作,不选择使用setInterval,因为setInterval性能开销较大,而且也不好设定时间间隔,不如requestAnimationFrame方便快捷。
可以看到上面的requestAnimationFrame回调了execIntervalBtnCheck函数,该函数用于处理计时之后的操作,判断当前剩下的按钮禁用时间还剩多少,是否禁用时间到期,代码如下:

// 上次处理的时间戳,为零代表没有任何需要处理的按钮
let lastCheckIntervalTime = 0
/**
 * @param { Number } currentTime requestAnimationFrame传给回调函数的时间戳
 */
function execIntervalBtnCheck(currentTime) {
  // 如果上次处理的时间戳时间为零,代表没有任何需要处理的按钮,而现在处于执行,说明被上面`document.body`的`click`调用,所以先将当前时间记录下来
  lastCheckIntervalTime === 0 && (lastCheckIntervalTime = currentTime)
  // 计算上次执行的时间与本次执行的时间的时间差
  const diffTime = currentTime - lastCheckIntervalTime
  // 对于禁用的按钮列表进行遍历处理
  const newIntervalList = intervalList.map(item => {
    const { target } = item
    // 计算当前按钮的剩余时间
    item.time -= diffTime
    // 剩余时间小于等于零时,解除按钮的禁用
    if (item.time <= 0) {
      target.classList.remove('showInterval')
      target.dataset.tip = ''
      target.disabled = false
      return false
    }
    // 剩余时间大于等于零时,更新剩余时间提示
    target.dataset.tip = Math.ceil(item.time / 1000) + '秒后可点击'
    return item
  }).filter(item => item) // filter过滤掉不再禁用的按钮
  // 更新禁用按钮列表
  intervalList = newIntervalList
  // 记录当前时间戳,下次调用时,用于计算时间差
  lastCheckIntervalTime = currentTime
  if (intervalList.length > 0) {
    // 如果还有禁用的按钮,开启下一次计时处理
    intervalBtnTimer = requestAnimationFrame(execIntervalBtnCheck)
  } else {
    // 如果没有禁用的按钮,关闭计时处理,重置所有标志
    lastCheckIntervalTime = 0
    intervalBtnTimer = 0
  }
}

至此,就实现了需求的功能。而且,对于源代码的改动也不大,只需要在每个需要限制的按钮上添加data-intervaldata-time即可,处理的逻辑也钮本身的逻辑无关,保证不会影响员有的功能。