一个函数搞定一个连点计数器🖱️

前言

彦祖们,在我们日常开发中,经常会遇到这样的需求

连续点击某个按钮多次,才触发某个事件,这时就需要一个连点计数器

阅读本文不依赖任何框架源码,会一点 js 即可

场景描述

手机的开发者模式,需要我们快速点击 7 次版本信息,才弹出开发者弹窗

笔者的业务场景也非常类似,需要连续点击某个按钮 7 次,才弹出配置弹窗(下文以 console 模拟)

场景复现

下面就让我们示例来复现下业务场景

代码结构

先来看下我们的 html 结构,结构非常简单

html 复制代码
<body style="height: 100vh;">
  <button>配置弹窗</button>
</body>
<script>
// todo ...
<script>

我们点击 7 次 button 按钮,需要触发弹窗事件

搬砖模式

先看下日常搬砖写法(cv就完事了,快!准!稳!)

js 复制代码
const button = document.querySelector('button')
let timer = null;
let count = 0;
button.addEventListener('click',function(e){
  clearTimeout(timer)
  if(count === 7){
    // do something
    console.log('配置弹窗')
  }
  count++
  timer = setTimeout(() => {
    count = 0
  },500)
})

我们利用一个定时器初始化 count=0,每次点击事件会清空这个定时器

也就是说 500ms 内没有点击事件那么 count=0 否则 count++

单次场景

但是在某些场景下,我们可能需要在点击 7 次后,取消后续的点击事件

比如说我们全局只允许一次弹窗

现有的代码是无法满足的(每点击 7 次,都会新增一个弹窗)

那么我们该如何限制这种场景呢?

其实非常简单,我们可以手动维护一个 cleanup,用于表示函数功能是否被清除(本文中用 destroy 更合适)

js 复制代码
const button = document.querySelector('button')
let timer = null;
let count = 0;
let cleanup = false // 取消后续的点击事件
button.addEventListener('click',function(e){
  if(cleanup) return // 已清除,则直接返回
  clearTimeout(timer)
  if(count === 10){
    cleanup = true
    console.log('弹窗配置')
  }
  count++
  timer = setTimeout(() => {
    count = 0
  },500)
})

这样我们就能维护一个简单的单次计数器了

函数模式

现在的这份代码,其实已经可以满足我们的业务搬砖需求了

但我们发现代码中存在一些重复的逻辑

在未来的业务开发中,我们还需要实现类似的业务需求

那我们就要一次次重复的 Ctrl+cv,日积月累项目中可能会存在大量的重复代码

高耦合性代码也会出现很多隐患

比如相同的命名,在其他代码中清除了 timer 定时器导致意外的 bug (不过短期内并不会出大问题🌚,开发业务快就完事了~)

言归正传,下面就让我们封装一个减少搬砖量的函数来解决这些问题吧

函数设计

面相重复的业务需求,我们更希望的是,把重复的逻辑封装起来

这时候就需要考虑如何设计这个功能了

该如何下手呢? 作为前端工程师,想必你一定用过防抖节流函数

停下来想一想,这个场景和防抖节流有半毛钱关系吗?

没错!在这个场景下我们就可以借鉴防抖节流的函数设计思想,其核心就是利用闭包函数

再谈闭包

闭包在面试八股文中,也是高频的考点,面试官经常会出一道题:手写一个防抖函数

但可惜的是,不少开发者八股文烂熟于心

一旦应用到业务场景中,一时却无从下手

重设计,轻代码

其实有了设计思路,代码都是水到渠成的流水潺潺罢了

看下此时的函数大致签名

js 复制代码
const useClickRunner = (callback) => {
  // ...
  return function runner(e){
    // ....
  }
}

结构非常简单,我们接收一个回调函数,返回一个执行函数

简易代码

有了以上函数结构,一个简易代码也就呼之欲出了

js 复制代码
const useClickRunner = (callback) => {
  let count = 0
  let timer = null
  return function(e){
    clearTimeout(timer)
    count++
    timer = setTimeout(() => {
      count = 0
    },500)
    callback(count) // 回调逻辑,把 count 交给开发者,自行处理业务逻辑
  }
}

这样我们就实现了一个最简单的连点计数器函数,使用也非常简单

js 复制代码
const button  = document.querySelector('button')

const clickMethod = useClickRunner((count) => {
  // do something
  if(count === 7){
    console.log('配置弹窗')
  }
})

button.addEventListener('click',clickMethod)

改进 cleanup

在上文中的 cleanup 我们使用的是一个标记,并不利于业务二次开发

受益于 Vue.js 设计与实现一书中的第四章响应系统的作用与实现

文中出现了 cleanup 函数(其实 4.11 的 onInvalidate 函数传参更符合我们的思想)

参考其设计思想,我们也把它设计为一个函数,供开发者自行调用

js 复制代码
const useClickRunner = (callback) => {
  let count = 0
  let timer = null
  let hasCleanup = false
  const cleanup = () => {
    hasCleanup = true
    clearTimeout(timer)
  }
  return function(e){
    if(hasCleanup) return
    clearTimeout(timer)
    count++
    timer = setTimeout(() => {
      count = 0
    },500)
    callback(count,cleanup) // 提供cleanup给开发者,供自行调用
  }
}

这样,我们就可以在业务中,通过调用 cleanup 函数,取消后续的点击事件了

js 复制代码
const button  = document.querySelector('button')
const clickMethod = useClickRunner((count,cleanup) => {
  // do something
  if(count === 7) {
    console.log('配置弹窗')
    cleanup() // 想什么时候取消 就什么时候取消
  }
})
button.addEventListener('click',clickMethod)

完整代码

当然,我们还可以进一步封装,把一些配置项,通过 options 参数传递给函数

这就比较简单了,不再赘述

js 复制代码
const useClickRunner = (callback,options={}) => {
  // validInterval: 有效连续点击间隔时间
  // initialCount: 初始点击次数(某些场景可能不是从 0 开始)
  const {validInterval = 100,initialCount = 0} = options

  let count = initialCount
  let timer = null
  let hasCleanup = false
  const cleanup = () => {
    hasCleanup = true
    clearTimeout(timer)
  }
  return function(e){
    if(hasCleanup) return
    clearTimeout(timer)
    count++
    timer = setTimeout(() => {
      count = initialCount
    },validInterval)
    callback(count,cleanup)
  }
}

使用方式

js 复制代码
const button = document.querySelector('button')
const clickMethod = useClickRunner(
  (count,cleanup) => {
    if(count === 12) {
      console.log('弹窗配置') // count 为 12 的时候弹窗
      cleanup()
    }
  },
  {
    validInterval:50, // 有效间隔时间为 50ms (淘汰手速慢的)
    initialCount:5 // 初始点击次数为 5
  }
)
button.addEventListener('click',clickMethod)

总结

至此我们就完成了一个高内聚低耦合,开发者体验友好的连点计数器函数

本文的核心还是围绕闭包函数展开

闭包函数真是前端开发过不去的一道坎儿~

写在最后

文中函数只覆盖了一小部分场景,彦祖们遇到类似的业务场景还能继续拓展该函数

如果有好的想法,可以评论区留言~

感谢彦祖们的阅读

个人能力有限

如有不对,欢迎指正 🌟 如有帮助,建议小心心大拇指三连🌟

相关推荐
m0_7482561411 分钟前
前端 MYTED单篇TED词汇学习功能优化
前端·学习
小马哥编程1 小时前
Function.prototype和Object.prototype 的区别
javascript
小白学前端6661 小时前
React Router 深入指南:从入门到进阶
前端·react.js·react
web130933203982 小时前
前端下载后端文件流,文件可以下载,但是打不开,显示“文件已损坏”的问题分析与解决方案
前端
王小王和他的小伙伴2 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱2 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿2 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
小k_不小2 小时前
C++面试八股文:指针与引用的区别
c++·面试
好名字08212 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
摇光933 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务