HarmonyOS ClickUtil 节流与防抖:彻底搞懂按钮防重复点击
目录
- 概述
- 一、为什么需要节流和防抖?
- 二、节流 vs 防抖,傻傻分不清?
- 三、ClickUtil 源码解析
- 3.1 核心状态
- 3.2 throttle — 节流实现
- 3.3 debounce — 防抖实现
- 四、完整 Demo 演示
- 4.1 节流 Demo
- 4.2 防抖 Demo
- 4.3 节流 UI
- 4.4 防抖 UI
- 五、运行效果对比
- 六、如何选择节流还是防抖?
- 七、API 速查表
- 八、小结
概述
近期发现一款很有意思的HarmonyOS 三方库, 地址 @pura/harmony-utils(V1.4.0) , 作者是"桃花镇童长老", 我这里也是直接通过该作者公布的源码进行案例编写进行,写了到目前写了一部分demo ,感觉确实很有帮助,这里呢也是开始写一个系列的演示demo 供大家参考。如有帮助可以在OpenHarmony中进行下载安装进行使用哦
案例demo导航展示
↓↓↓↓↓↓接下来言归正传 ↓↓↓↓
一、为什么需要节流和防抖?
在移动应用中,用户往往会在极短时间内连续点击按钮。如果没有任何防护措施:
- 重复提交表单:用户点了"提交"后因网络慢又点了几次,服务器收到多个相同请求
- 重复触发导航:点击跳转按钮连续触发,路由栈里压了多个相同页面
- 接口被刷:搜索框每次击键都请求接口,每秒钟可能发起几十个请求
节流(Throttle)和防抖(Debounce)是解决这类问题的经典方案。
二、节流 vs 防抖,傻傻分不清?
用一个形象的比喻来区分:
节流:就像地铁的闸机,在规定时间内只让一个人通过,后面的人必须等前一个人完全通过后才能进入。适合"限制频率"的场景。
防抖:就像电梯门,只要还有人进来,电梯门就一直等待。直到没有人进来(停止操作),等待一段时间后才关门执行。适合"等待停止"的场景。
| 节流(Throttle) | 防抖(Debounce) | |
|---|---|---|
| 核心逻辑 | 固定时间内只执行一次 | 最后一次操作后延迟执行 |
| 触发时机 | 立即执行或延迟执行 | 停止操作后执行 |
| 适合场景 | 防重复提交、滚动事件 | 搜索输入、窗口缩放 |
三、ClickUtil 源码解析
3.1 核心状态
exportclassClickUtil{privatestaticthrottleTimeoutID:number;//节流timeoutIDprivatestaticflag:boolean=false;//节流flag,true=已经进入执行状态了privatestaticdefaultId:string=DateUtil.getTodayTime().toString();//防抖IdthrottleTimeoutID:存储节流定时器的 ID,用于在等待期结束后重置flagflag:节流的门控标志,true表示正在节流冷却期defaultId:防抖的默认 ID(用时间戳初始化),当只有一个防抖事件时使用
3.2 throttle — 节流实现
staticthrottle(func:()=>void,wait:number=1000,immediate:boolean=true){if(immediate){if(!ClickUtil.flag){ClickUtil.flag=true;typeoffunc==='function'&&func();ClickUtil.throttleTimeoutID=setTimeout(()=>{ClickUtil.flag=false;clearTimeout(ClickUtil.throttleTimeoutID);},wait);}}else{if(!ClickUtil.flag){ClickUtil.flag=true;ClickUtil.throttleTimeoutID=setTimeout(()=>{ClickUtil.flag=false;typeoffunc==='function'&&func();clearTimeout(ClickUtil.throttleTimeoutID);},wait);}}}参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
func | () => void | 必填 | 要执行的回调函数 |
wait | number | 1000 | 节流冷却时间(毫秒) |
immediate | boolean | true | true=立即执行后冷却,false=冷却结束后执行 |
两种模式的时序对比:
immediate = true(立即模式):
点击 → 立即执行 → 冷却 1000ms → 可以再次点击immediate = false(延迟模式):
点击 → 冷却 1000ms → 执行 → 可以再次点击3.3 debounce — 防抖实现
staticdebounce(func:()=>void,wait:number=1000,clickId:string=ClickUtil.defaultId){letcacheID=CacheUtil.get<number>(`ClickUtil_debounce_timeoutID_${clickId}`);//获取idif(cacheID!==undefined&&cacheID!==null){clearTimeout(cacheID);}lettimeoutID=setTimeout(()=>{typeoffunc==='function'&&func();clearTimeout(timeoutID);},wait);CacheUtil.put<number>(`ClickUtil_debounce_timeoutID_${clickId}`,timeoutID);//缓存id}参数说明:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
func | () => void | 必填 | 要执行的回调函数 |
wait | number | 1000 | 防抖等待时间(毫秒) |
clickId | string | defaultId | 事件标识,用于区分多个独立的防抖事件 |
防抖的工作原理:
- 每次触发时,先从
CacheUtil中取出上一次的timeoutID - 如果存在,调用
clearTimeout取消上一次的定时器 - 重新设置一个新的定时器
- 将新的
timeoutID存入CacheUtil
这样,只要在wait毫秒内持续点击,定时器一直被重置;只有停止点击并等待wait毫秒后,函数才会执行。
多事件 ID 的设计:
通过clickId参数,可以维护多个独立的防抖事件:
// 搜索框防抖ClickUtil.debounce(()=>this.doSearch(),500,'search_input');// 评论提交防抖ClickUtil.debounce(()=>this.submitComment(),1000,'comment_submit');两个事件在CacheUtil中的 key 分别是:
ClickUtil_debounce_timeoutID_search_inputClickUtil_debounce_timeoutID_comment_submit
互不干扰。
四、完整 Demo 演示
来自CacheCharClickDemoPage.ets的 ClickUtil 演示部分:
4.1 节流 Demo
@StatethrottleCount:number=0;@StatethrottleImmediate:boolean=true;doThrottle(){ClickUtil.throttle(()=>{this.throttleCount++;this.addLog('Throttle',`触发!累计:${this.throttleCount}次`,'success');},1500,this.throttleImmediate);}4.2 防抖 Demo
@StatedebounceCount:number=0;@StatedebounceDelay:number=1000;doDebounce(){ClickUtil.debounce(()=>{this.debounceCount++;this.addLog('Debounce',`执行!累计:${this.debounceCount}次`,'success');},this.debounceDelay,'demo_debounce');}4.3 节流 UI
// 模式选择Row(){Text('触发模式:').fontSize(12).fontColor('#888')Radio({value:'imm',group:'throttleMode'}).checked(this.throttleImmediate).onChange((v:boolean)=>{if(v)this.throttleImmediate=true;})Text('立即执行').fontSize(12).margin({left:2}).onClick(()=>{this.throttleImmediate=true;})Radio({value:'delayed',group:'throttleMode'}).checked(!this.throttleImmediate).onChange((v:boolean)=>{if(v)this.throttleImmediate=false;})Text('延迟执行').fontSize(12).margin({left:2}).onClick(()=>{this.throttleImmediate=false;})}.margin({bottom:10})// 触发按钮Button('快速连续点击我(节流)').width('100%').height(48).borderRadius(10).backgroundColor('#4080FF').fontColor('#FFF').fontSize(14).onClick(()=>{this.doThrottle();})4.4 防抖 UI
// 延迟时间调节Row(){Text('延迟时间:').fontSize(12).fontColor('#888')Slider({value:this.debounceDelay,min:300,max:3000,step:100,style:SliderStyle.OutSet}).layoutWeight(1).showTips(true).onChange((v:number)=>{this.debounceDelay=v;})Text(`${this.debounceDelay}ms`).fontSize(12).fontColor('#4080FF').margin({left:8})}.margin({bottom:10})// 触发按钮Button('快速连续点击我(防抖)').width('100%').height(48).borderRadius(10).backgroundColor('#FF9800').fontColor('#FFF').fontSize(14).onClick(()=>{this.doDebounce();})五、运行效果对比
节流测试(immediate=true,间隔 1500ms):
在 1.5 秒内快速点击 10 次,日志只会出现 1 条:
[Throttle] 触发!累计: 1 次等 1.5 秒后再次点击,才会新增一条日志。
防抖测试(延迟 1000ms):
快速点击 10 次,不会立即有日志。停止点击,等 1 秒后才出现 1 条:
[Debounce] 执行!累计: 1 次六、如何选择节流还是防抖?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 提交按钮防重复 | 节流(immediate=true) | 第一次点击立即响应,后续冷却 |
| 搜索框实时搜索 | 防抖 | 等用户停止输入后才请求 |
| 页面滚动事件 | 节流 | 固定频率触发,不能太密集 |
| 窗口 resize | 防抖 | 等缩放停止后再更新布局 |
| 点赞/收藏按钮 | 节流(immediate=true) | 第一次立即反馈,避免重复 |
| 验证码发送 | 节流(较长wait) | 60 秒内只能发送一次 |
七、API 速查表
| 方法 | 参数 | 说明 |
|---|---|---|
throttle(fn, wait, immediate) | fn: 回调;wait: 间隔毫秒(默认1000);immediate: 是否立即执行(默认true) | 节流,固定时间窗口内只执行一次 |
debounce(fn, wait, clickId) | fn: 回调;wait: 等待毫秒(默认1000);clickId: 事件ID | 防抖,停止触发后延迟执行 |
八、小结
ClickUtil将两种经典的交互优化方案封装为简洁的静态方法:
throttle:适合需要"限制频率"的场景,immediate参数控制是先执行还是后执行debounce:适合需要"等待停止"的场景,clickId参数支持多个独立防抖事件
底层依赖CacheUtil存储防抖定时器 ID,这也体现了工具类之间协作的设计思想。在任何需要防止重复操作的场景,ClickUtil都是首选方案。
