詳解Java中的防抖和節(jié)流
概念
防抖(debounce)
當(dāng)持續(xù)觸發(fā)事件時(shí),一定時(shí)間段內(nèi)沒(méi)有再觸發(fā)事件,事件處理函數(shù)才會(huì)執(zhí)行一次,如果設(shè)定時(shí)間到來(lái)之前,又觸發(fā)了事件,就重新開(kāi)始延時(shí)。
- 防抖,即如果短時(shí)間內(nèi)大量觸發(fā)同一事件,都會(huì)重置計(jì)時(shí)器,等到事件不觸發(fā)了,再等待規(guī)定的事件,才會(huì)執(zhí)行函數(shù)。而這整個(gè)過(guò)程就觸發(fā)了一次點(diǎn)贊函數(shù)到服務(wù)器。原理:設(shè)置一個(gè)定時(shí)器,設(shè)置在規(guī)定的時(shí)間后觸發(fā)事件處理,每次觸發(fā)事件都會(huì)重置計(jì)時(shí)器。
- 舉例:很簡(jiǎn)單的例子,就是如果你瘋狂的給朋友圈點(diǎn)贊再取消點(diǎn)贊,這個(gè)過(guò)程都會(huì)把計(jì)時(shí)器清空,等到你點(diǎn)累了不點(diǎn)了,等待0.5秒,才會(huì)觸發(fā)函數(shù),把你最終結(jié)果傳給服務(wù)器。
- 問(wèn)題1:那既然是這樣,讓前端做防抖不就好了嘛。答案是可以,但是會(huì)失去用戶體驗(yàn)。本來(lái)有的用戶點(diǎn)贊就是為了玩,現(xiàn)在你前端直接提示操作太快~請(qǐng)歇會(huì)。用戶是不是就失去了樂(lè)趣,這一點(diǎn)還得參考QQ空間的點(diǎn)贊,雖然我不知道它是不是用了防抖,但是他把點(diǎn)贊,取消點(diǎn)贊做成了動(dòng)畫(huà),這樣每次用戶操作的時(shí)候,都會(huì)跳出執(zhí)行動(dòng)畫(huà),大大增加了用戶的體驗(yàn)性。
- 問(wèn)題2:那么問(wèn)題來(lái)了,在一定時(shí)間內(nèi),一直點(diǎn)擊,就會(huì)重置計(jì)時(shí)器。那要是點(diǎn)擊一天一夜,是不是他就不會(huì)在執(zhí)行了呢。理論上是這樣,但是人會(huì)累的嘛??偛荒芤恢睉?zhàn)斗是吧。所以人做不到,只能是機(jī)器、腳本來(lái)處理了,那也正好,防抖還能用來(lái)阻擋部分腳本攻擊。
節(jié)流(throttle)
當(dāng)持續(xù)觸發(fā)事件時(shí),保證在一定時(shí)間內(nèi)只調(diào)用一次事件處理函數(shù),意思就是說(shuō),假設(shè)一個(gè)用戶一直觸發(fā)這個(gè)函數(shù),且每次觸發(fā)小于既定值,函數(shù)節(jié)流會(huì)每隔這個(gè)時(shí)間調(diào)用一次。
想到這里,很多人就會(huì)想到一個(gè)作用,沒(méi)錯(cuò),就是防重復(fù)提交。但是這個(gè)業(yè)務(wù)時(shí)間比較難把控,所以還是建議用它來(lái)做一些無(wú)狀態(tài)的操作比較好。比如說(shuō):刷新排行榜,前端看似一直在點(diǎn)擊,其實(shí)后端為了防止接口崩掉,每1秒才執(zhí)行真正的一次刷新。
區(qū)別
防抖是將多次執(zhí)行變?yōu)橹付〞r(shí)間內(nèi)不在觸發(fā)之后,執(zhí)行一次。
節(jié)流是將多次執(zhí)行變?yōu)橹付〞r(shí)間不論觸發(fā)多少次,時(shí)間一到就執(zhí)行一次
Java實(shí)現(xiàn)
java實(shí)現(xiàn)防抖和節(jié)流的關(guān)鍵是Timer類(lèi)和Runnable接口。
其中,Timer中關(guān)鍵方法cancel() 實(shí)現(xiàn)防抖 schedule() 實(shí)現(xiàn)節(jié)流。下面簡(jiǎn)單介紹一下這兩個(gè)方法。
Timer##cancel():Timer.cancel() 被調(diào)用之后整個(gè)Timer 的 線程都會(huì)結(jié)束掉。
這就很好理解了,既然是做防抖,只要你在指定時(shí)間內(nèi)觸發(fā),我直接 cancel()掉,就是取消掉,不讓他執(zhí)行。
Timer##schedule():用戶調(diào)用 schedule() 方法后,要等待N秒的時(shí)間才可以第一次執(zhí)行 run() 方法。
這個(gè)N是我們根據(jù)業(yè)務(wù)評(píng)估出來(lái)的時(shí)間,作為參數(shù)傳進(jìn)去。
防抖(debounce)
package com.example.test01.zhangch;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author zhangch
* @Description java 防抖
* @Date 2022/8/4 18:18
* @Version 1.0
*/
@SuppressWarnings("all")
public class DebounceTask {
/**
* 防抖實(shí)現(xiàn)關(guān)鍵類(lèi)
*/
private Timer timer;
/**
* 防抖時(shí)間:根據(jù)業(yè)務(wù)評(píng)估
*/
private Long delay;
/**
* 開(kāi)啟線程執(zhí)行任務(wù)
*/
private Runnable runnable;
public DebounceTask(Runnable runnable, Long delay) {
this.runnable = runnable;
this.delay = delay;
}
/**
*
* @param runnable 要執(zhí)行的任務(wù)
* @param delay 執(zhí)行時(shí)間
* @return 初始化 DebounceTask 對(duì)象
*/
public static DebounceTask build(Runnable runnable, Long delay){
return new DebounceTask(runnable, delay);
}
//Timer類(lèi)執(zhí)行:cancel()-->取消操作;schedule()-->執(zhí)行操作
public void timerRun(){
//如果有任務(wù),則取消不執(zhí)行(防抖實(shí)現(xiàn)的關(guān)鍵)
if(timer!=null){
timer.cancel();
}
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
//把 timer 設(shè)置為空,這樣下次判斷它就不會(huì)執(zhí)行了
timer=null;
//執(zhí)行 runnable 中的 run()方法
runnable.run();
}
}, delay);
}
}
防抖測(cè)試1
可以看到,測(cè)試中,我 1 毫秒請(qǐng)求一次,這樣的話,1秒內(nèi)都存在連續(xù)請(qǐng)求,防抖操作永遠(yuǎn)不會(huì)執(zhí)行。
public static void main(String[] args){
//構(gòu)建對(duì)象,1000L: 1秒執(zhí)行-->1秒內(nèi)沒(méi)有請(qǐng)求,在執(zhí)行防抖操作
DebounceTask task = DebounceTask.build(new Runnable() {
@Override
public void run() {
System.out.println("防抖操作執(zhí)行了:do task: "+System.currentTimeMillis());
}
},1000L);
long delay = 100;
while (true){
System.out.println("請(qǐng)求執(zhí)行:call task: "+System.currentTimeMillis());
task.timerRun();
try {
//休眠1毫秒在請(qǐng)求
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結(jié)果如我們所料:
Connected to the target VM, address: '127.0.0.1:5437', transport: 'socket'
請(qǐng)求執(zhí)行:call task: 1659609433021
請(qǐng)求執(zhí)行:call task: 1659609433138
請(qǐng)求執(zhí)行:call task: 1659609433243
請(qǐng)求執(zhí)行:call task: 1659609433350
請(qǐng)求執(zhí)行:call task: 1659609433462
請(qǐng)求執(zhí)行:call task: 1659609433572
請(qǐng)求執(zhí)行:call task: 1659609433681
請(qǐng)求執(zhí)行:call task: 1659609433787
請(qǐng)求執(zhí)行:call task: 1659609433893
請(qǐng)求執(zhí)行:call task: 1659609433999
請(qǐng)求執(zhí)行:call task: 1659609434106
請(qǐng)求執(zhí)行:call task: 1659609434215
請(qǐng)求執(zhí)行:call task: 1659609434321
請(qǐng)求執(zhí)行:call task: 1659609434425
請(qǐng)求執(zhí)行:call task: 1659609434534
防抖測(cè)試2
測(cè)試2中,我們?cè)谡?qǐng)求了2秒之后,讓主線程休息2秒,這個(gè)時(shí)候,防抖在1秒內(nèi)沒(méi)有在次觸發(fā),所以就會(huì)執(zhí)行一次防抖操作。
public static void main(String[] args){
//構(gòu)建對(duì)象,1000L:1秒執(zhí)行
DebounceTask task = DebounceTask.build(new Runnable() {
@Override
public void run() {
System.out.println("防抖操作執(zhí)行了:do task: "+System.currentTimeMillis());
}
},1000L);
long delay = 100;
long douDelay = 0;
while (true){
System.out.println("請(qǐng)求執(zhí)行:call task: "+System.currentTimeMillis());
task.timerRun();
douDelay = douDelay+100;
try {
//如果請(qǐng)求執(zhí)行了兩秒,我們讓他先休息兩秒,在接著請(qǐng)求
if (douDelay == 2000){
Thread.sleep(douDelay);
}
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結(jié)果如我們所料,防抖任務(wù)觸發(fā)了一次,休眠結(jié)束之后,請(qǐng)求就會(huì)在1毫秒內(nèi)連續(xù)觸發(fā),防抖也就不會(huì)在次觸發(fā)了。
請(qǐng)求執(zhí)行:call task: 1659609961816
請(qǐng)求執(zhí)行:call task: 1659609961924
請(qǐng)求執(zhí)行:call task: 1659609962031
請(qǐng)求執(zhí)行:call task: 1659609962138
請(qǐng)求執(zhí)行:call task: 1659609962245
請(qǐng)求執(zhí)行:call task: 1659609962353
防抖操作執(zhí)行了:do task: 1659609963355
請(qǐng)求執(zhí)行:call task: 1659609964464
請(qǐng)求執(zhí)行:call task: 1659609964569
請(qǐng)求執(zhí)行:call task: 1659609964678
請(qǐng)求執(zhí)行:call task: 1659609964784
防抖測(cè)試簡(jiǎn)易版
簡(jiǎn)易版:根據(jù)新手寫(xiě)代碼習(xí)慣,對(duì)代碼寫(xiě)法做了調(diào)整,但是不影響整體功能。這種寫(xiě)法更加符合我這種新手小白的寫(xiě)法。
public static void main(String[] args){
//要執(zhí)行的任務(wù),因?yàn)?Runnable 是接口,所以 new 對(duì)象的時(shí)候要實(shí)現(xiàn)它的 run方法
Runnable runnable = new Runnable() {
@Override
public void run() {
//執(zhí)行打印,真實(shí)開(kāi)發(fā)中,是這些我們的業(yè)務(wù)代碼。
System.out.println("防抖操作執(zhí)行了:do task: "+System.currentTimeMillis());
}
};
//runnable:要執(zhí)行的任務(wù),通過(guò)參數(shù)傳遞進(jìn)去。1000L:1秒執(zhí)行內(nèi)沒(méi)有請(qǐng)求,就執(zhí)行一次防抖操作
DebounceTask task = DebounceTask.build(runnable,1000L);
//請(qǐng)求持續(xù)時(shí)間
long delay = 100;
//休眠時(shí)間,為了讓防抖任務(wù)執(zhí)行
long douDelay = 0;
//while 死循環(huán),請(qǐng)求一直執(zhí)行
while (true){
System.out.println("請(qǐng)求執(zhí)行:call task: "+System.currentTimeMillis());
//調(diào)用 DebounceTask 防抖類(lèi)中的 timerRun() 方法, 執(zhí)行防抖任務(wù)
task.timerRun();
douDelay = douDelay+100;
try {
//如果請(qǐng)求執(zhí)行了兩秒,我們讓他先休息兩秒,在接著請(qǐng)求
if (douDelay == 2000){
Thread.sleep(douDelay);
}
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
節(jié)流(throttle)
package com.example.test01.zhangch;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author zhangch
* @Description 節(jié)流
* @Date 2022/8/6 15:41
* @Version 1.0
*/
public class ThrottleTask {
/**
* 節(jié)流實(shí)現(xiàn)關(guān)鍵類(lèi)
*/
private Timer timer;
private Long delay;
private Runnable runnable;
private boolean needWait=false;
/**
* 有參構(gòu)造函數(shù)
* @param runnable 要啟動(dòng)的定時(shí)任務(wù)
* @param delay 延遲時(shí)間
*/
public ThrottleTask(Runnable runnable, Long delay) {
this.runnable = runnable;
this.delay = delay;
this.timer = new Timer();
}
/**
* build 創(chuàng)建對(duì)象,相當(dāng)于 ThrottleTask task = new ThrottleTask();
* @param runnable 要執(zhí)行的節(jié)流任務(wù)
* @param delay 延遲時(shí)間
* @return ThrottleTask 對(duì)象
*/
public static ThrottleTask build(Runnable runnable, Long delay){
return new ThrottleTask(runnable, delay);
}
public void taskRun(){
//如果 needWait 為 false,結(jié)果取反,表達(dá)式為 true。執(zhí)行 if 語(yǔ)句
if(!needWait){
//設(shè)置為 true,這樣下次就不會(huì)再執(zhí)行
needWait=true;
//執(zhí)行節(jié)流方法
timer.schedule(new TimerTask() {
@Override
public void run() {
//執(zhí)行完成,設(shè)置為 false,讓下次操作再進(jìn)入 if 語(yǔ)句中
needWait=false;
//開(kāi)啟多線程執(zhí)行 run() 方法
runnable.run();
}
}, delay);
}
}
}
節(jié)流測(cè)試1
節(jié)流測(cè)試,每 2ms 請(qǐng)求一次,節(jié)流任務(wù)是每 1s 執(zhí)行一次。真實(shí)效果應(yīng)該是 1s 內(nèi)前端發(fā)起了五次請(qǐng)求,但是后端只執(zhí)行了一次操作
public static void main(String[] args){
//創(chuàng)建節(jié)流要執(zhí)行的對(duì)象,并把要執(zhí)行的任務(wù)傳入進(jìn)去
ThrottleTask task = ThrottleTask.build(new Runnable() {
@Override
public void run() {
System.out.println("節(jié)流任務(wù)執(zhí)行:do task: "+System.currentTimeMillis());
}
},1000L);
//while一直執(zhí)行,模擬前端用戶一直請(qǐng)求后端
while (true){
System.out.println("前端請(qǐng)求后端:call task: "+System.currentTimeMillis());
task.taskRun();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
結(jié)果如我們所料
前端請(qǐng)求后端:call task: 1659772459363
前端請(qǐng)求后端:call task: 1659772459574
前端請(qǐng)求后端:call task: 1659772459780
前端請(qǐng)求后端:call task: 1659772459995
前端請(qǐng)求后端:call task: 1659772460205
節(jié)流任務(wù)執(zhí)行:do task: 1659772460377
前端請(qǐng)求后端:call task: 1659772460409
前端請(qǐng)求后端:call task: 1659772460610
前端請(qǐng)求后端:call task: 1659772460812
前端請(qǐng)求后端:call task: 1659772461027
前端請(qǐng)求后端:call task: 1659772461230
節(jié)流任務(wù)執(zhí)行:do task: 1659772461417
彩蛋
idea 爆紅線了,強(qiáng)迫癥的我受不了,肯定要解決它

解決方法1
腦子第一時(shí)間冒出來(lái)的是 @SuppressWarnings("all") 注解,跟所有的警告說(shuō)拜拜~瞬間就清爽了

解決方法2
算了,壓制警告總感覺(jué)是不負(fù)責(zé)任??偛荒苓@樣草草了事,那就來(lái)直面這個(gè)爆紅。既然讓我用 ScheduledExecutorService ,那簡(jiǎn)單,直接替換
public class ThrottleTask {
/**
* 節(jié)流實(shí)現(xiàn)關(guān)鍵類(lèi):
*/
private ScheduledExecutorService timer;
private Long delay;
private Runnable runnable;
private boolean needWait=false;
/**
* 有參構(gòu)造函數(shù)
* @param runnable 要啟動(dòng)的定時(shí)任務(wù)
* @param delay 延遲時(shí)間
*/
public ThrottleTask(Runnable runnable, Long delay) {
this.runnable = runnable;
this.delay = delay;
this.timer = Executors.newSingleThreadScheduledExecutor();
}
/**
* build 創(chuàng)建對(duì)象,相當(dāng)于 ThrottleTask task = new ThrottleTask();
* @param runnable 要執(zhí)行的節(jié)流任務(wù)
* @param delay 延遲時(shí)間
* @return ThrottleTask 對(duì)象
*/
public static ThrottleTask build(Runnable runnable, Long delay){
return new ThrottleTask(runnable, delay);
}
public void taskRun(){
//如果 needWait 為 false,結(jié)果取反,表達(dá)式為 true。執(zhí)行 if 語(yǔ)句
if(!needWait){
//設(shè)置為 true,這樣下次就不會(huì)再執(zhí)行
needWait=true;
//執(zhí)行節(jié)流方法
timer.schedule(new TimerTask() {
@Override
public void run() {
//執(zhí)行完成,設(shè)置為 false,讓下次操作再進(jìn)入 if 語(yǔ)句中
needWait=false;
//開(kāi)啟多線程執(zhí)行 run() 方法
runnable.run();
}
}, delay,TimeUnit.MILLISECONDS);
}
}
}
那么定時(shí)器 Timer 和 ScheduledThreadPoolExecutor 解決方案之間的主要區(qū)別是什么,我總結(jié)了三點(diǎn)...
- 定時(shí)器對(duì)系統(tǒng)時(shí)鐘的變化敏感;ScheduledThreadPoolExecutor并不會(huì)。
- 定時(shí)器只有一個(gè)執(zhí)行線程;ScheduledThreadPoolExecutor可以配置任意數(shù)量的線程。
- TimerTask中拋出的運(yùn)行時(shí)異常會(huì)殺死線程,因此后續(xù)的計(jì)劃任務(wù)不會(huì)繼續(xù)運(yùn)行;使用ScheduledThreadExecutor–當(dāng)前任務(wù)將被取消,但其余任務(wù)將繼續(xù)運(yùn)行。
到此這篇關(guān)于詳解Java中的防抖和節(jié)流的文章就介紹到這了,更多相關(guān)Java防抖 節(jié)流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)異步執(zhí)行的8種方式總結(jié)
這篇文章主要給大家介紹了關(guān)于Java實(shí)現(xiàn)異步執(zhí)行的8種方式,異步編程不會(huì)阻塞程序的執(zhí)行,它將耗時(shí)的操作提交給后臺(tái)線程或其他執(zhí)行環(huán)境,并立即返回,使得程序可以繼續(xù)執(zhí)行其他任務(wù),需要的朋友可以參考下2023-09-09
mybatis mybatis-plus-generator+clickhouse自動(dòng)生成代碼案例詳解
這篇文章主要介紹了mybatis mybatis-plus-generator+clickhouse自動(dòng)生成代碼案例詳解,本篇文章通過(guò)簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
java向微信服務(wù)號(hào)發(fā)送消息的完整步驟實(shí)例
這篇文章主要介紹了java向微信服務(wù)號(hào)發(fā)送消息的相關(guān)資料,包括申請(qǐng)測(cè)試號(hào)獲取appID/appsecret、關(guān)注公眾號(hào)獲取openID、配置消息模板及代碼實(shí)現(xiàn),需要的朋友可以參考下2025-06-06
JAVA解決在@autowired,@Resource注入為null的情況
這篇文章主要介紹了JAVA解決在@autowired,@Resource注入為null的情況,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-10-10
java控制臺(tái)實(shí)現(xiàn)學(xué)生信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了java控制臺(tái)實(shí)現(xiàn)學(xué)生信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
Spring?Boot+Vue實(shí)現(xiàn)Socket通知推送的完整步驟
最近工作中涉及消息通知功能的開(kāi)發(fā),所以下面這篇文章主要給大家介紹了關(guān)于Spring?Boot+Vue實(shí)現(xiàn)Socket通知推送的完整步驟,文中通過(guò)實(shí)例代碼以及圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04
SpringCloud集成Sleuth和Zipkin的思路講解
Zipkin 是 Twitter 的一個(gè)開(kāi)源項(xiàng)目,它基于 Google Dapper 實(shí)現(xiàn),它致力于收集服務(wù)的定時(shí)數(shù)據(jù),以及解決微服務(wù)架構(gòu)中的延遲問(wèn)題,包括數(shù)據(jù)的收集、存儲(chǔ)、查找和展現(xiàn),這篇文章主要介紹了SpringCloud集成Sleuth和Zipkin,需要的朋友可以參考下2022-11-11

