前言
彦祖们,在我们日常开发中,经常会遇到这样的需求
连续点击某个按钮多次,才触发某个事件,这时就需要一个连点计数器
阅读本文不依赖任何框架源码,会一点 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)
总结
至此我们就完成了一个高内聚低耦合,开发者体验友好的连点计数器
函数
本文的核心还是围绕闭包函数
展开
闭包函数
真是前端开发过不去的一道坎儿~
写在最后
文中函数只覆盖了一小部分场景,彦祖们遇到类似的业务场景还能继续拓展该函数
如果有好的想法,可以评论区留言~
感谢彦祖们的阅读
个人能力有限
如有不对,欢迎指正 🌟 如有帮助,建议小心心大拇指三连🌟