當持續觸發事件時,一定時間段內沒有再觸發事件,事件處理常式才會執行一次,如果設定時間到來之前,又觸發了事件,就重新開始延時。
防抖,即如果短時間內大量觸發同一事件,都會重置計時器,等到事件不觸發了,再等待規定的事件,才會執行函數。而這整個過程就觸發了一次點贊函數到伺服器。原理:設定一個定時器,設定在規定的時間後觸發事件處理,每次觸發事件都會重置計時器。
舉例:很簡單的例子,就是如果你瘋狂的給朋友圈點贊再取消點贊,這個過程都會把計時器清空,等到你點累了不點了,等待0.5秒,才會觸發函數,把你最終結果傳給伺服器。
問題1:那既然是這樣,讓前端做防抖不就好了嘛。答案是可以,但是會失去使用者體驗。本來有的使用者點贊就是為了玩,現在你前端直接提示操作太快~請歇會。使用者是不是就失去了樂趣,這一點還得參考QQ空間的點贊,雖然我不知道它是不是用了防抖,但是他把點贊,取消點贊做成了動畫,這樣每次使用者操作的時候,都會跳出執行動畫,大大增加了使用者的體驗性。
問題2:那麼問題來了,在一定時間內,一直點選,就會重置計時器。那要是點選一天一夜,是不是他就不會在執行了呢。理論上是這樣,但是人會累的嘛。總不能一直戰鬥是吧。所以人做不到,只能是機器、指令碼來處理了,那也正好,防抖還能用來阻擋部分指令碼攻擊。
當持續觸發事件時,保證在一定時間內只呼叫一次事件處理常式,意思就是說,假設一個使用者一直觸發這個函數,且每次觸發小於既定值,函數節流會每隔這個時間呼叫一次。
防抖是將多次執行變為指定時間內不在觸發之後,執行一次。
節流是將多次執行變為指定時間不論觸發多少次,時間一到就執行一次
java實現防抖和節流的關鍵是Timer類和Runnable介面。
其中,Timer中關鍵方法cancel() 實現防抖 schedule() 實現節流。下面簡單介紹一下這兩個方法。
Timer##cancel():Timer.cancel() 被呼叫之後整個Timer 的 執行緒都會結束掉。
Timer##schedule():使用者呼叫 schedule() 方法後,要等待N秒的時間才可以第一次執行 run() 方法。
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 {
/**
* 防抖實現關鍵類
*/
private Timer timer;
/**
* 防抖時間:根據業務評估
*/
private Long delay;
/**
* 開啟執行緒執行任務
*/
private Runnable runnable;
public DebounceTask(Runnable runnable, Long delay) {
this.runnable = runnable;
this.delay = delay;
}
/**
*
* @param runnable 要執行的任務
* @param delay 執行時間
* @return 初始化 DebounceTask 物件
*/
public static DebounceTask build(Runnable runnable, Long delay){
return new DebounceTask(runnable, delay);
}
//Timer類執行:cancel()-->取消操作;schedule()-->執行操作
public void timerRun(){
//如果有任務,則取消不執行(防抖實現的關鍵)
if(timer!=null){
timer.cancel();
}
timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
//把 timer 設定為空,這樣下次判斷它就不會執行了
timer=null;
//執行 runnable 中的 run()方法
runnable.run();
}
}, delay);
}
}
可以看到,測試中,我 1 毫秒請求一次,這樣的話,1秒內都存在連續請求,防抖操作永遠不會執行。
public static void main(String[] args){
//構建物件,1000L: 1秒執行-->1秒內沒有請求,在執行防抖操作
DebounceTask task = DebounceTask.build(new Runnable() {
@Override
public void run() {
System.out.println("防抖操作執行了:do task: "+System.currentTimeMillis());
}
},1000L);
long delay = 100;
while (true){
System.out.println("請求執行:call task: "+System.currentTimeMillis());
task.timerRun();
try {
//休眠1毫秒在請求
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Connected to the target VM, address: '127.0.0.1:5437', transport: 'socket'
請求執行:call task: 1659609433021
請求執行:call task: 1659609433138
請求執行:call task: 1659609433243
請求執行:call task: 1659609433350
請求執行:call task: 1659609433462
請求執行:call task: 1659609433572
請求執行:call task: 1659609433681
請求執行:call task: 1659609433787
請求執行:call task: 1659609433893
請求執行:call task: 1659609433999
請求執行:call task: 1659609434106
請求執行:call task: 1659609434215
請求執行:call task: 1659609434321
請求執行:call task: 1659609434425
請求執行:call task: 1659609434534
測試2中,我們在請求了2秒之後,讓主執行緒休息2秒,這個時候,防抖在1秒內沒有在次觸發,所以就會執行一次防抖操作。
public static void main(String[] args){
//構建物件,1000L:1秒執行
DebounceTask task = DebounceTask.build(new Runnable() {
@Override
public void run() {
System.out.println("防抖操作執行了:do task: "+System.currentTimeMillis());
}
},1000L);
long delay = 100;
long douDelay = 0;
while (true){
System.out.println("請求執行:call task: "+System.currentTimeMillis());
task.timerRun();
douDelay = douDelay+100;
try {
//如果請求執行了兩秒,我們讓他先休息兩秒,在接著請求
if (douDelay == 2000){
Thread.sleep(douDelay);
}
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
請求執行:call task: 1659609961816
請求執行:call task: 1659609961924
請求執行:call task: 1659609962031
請求執行:call task: 1659609962138
請求執行:call task: 1659609962245
請求執行:call task: 1659609962353
防抖操作執行了:do task: 1659609963355
請求執行:call task: 1659609964464
請求執行:call task: 1659609964569
請求執行:call task: 1659609964678
請求執行:call task: 1659609964784
簡易版:根據新手寫程式碼習慣,對程式碼寫法做了調整,但是不影響整體功能。這種寫法更加符合我這種新手小白的寫法。
public static void main(String[] args){
//要執行的任務,因為 Runnable 是介面,所以 new 物件的時候要實現它的 run方法
Runnable runnable = new Runnable() {
@Override
public void run() {
//執行列印,真實開發中,是這些我們的業務程式碼。
System.out.println("防抖操作執行了:do task: "+System.currentTimeMillis());
}
};
//runnable:要執行的任務,通過引數傳遞進去。1000L:1秒執行內沒有請求,就執行一次防抖操作
DebounceTask task = DebounceTask.build(runnable,1000L);
//請求持續時間
long delay = 100;
//休眠時間,為了讓防抖任務執行
long douDelay = 0;
//while 死迴圈,請求一直執行
while (true){
System.out.println("請求執行:call task: "+System.currentTimeMillis());
//呼叫 DebounceTask 防抖類中的 timerRun() 方法, 執行防抖任務
task.timerRun();
douDelay = douDelay+100;
try {
//如果請求執行了兩秒,我們讓他先休息兩秒,在接著請求
if (douDelay == 2000){
Thread.sleep(douDelay);
}
Thread.sleep(delay);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
package com.example.test01.zhangch;
import java.util.Timer;
import java.util.TimerTask;
/**
* @Author zhangch
* @Description 節流
* @Date 2022/8/6 15:41
* @Version 1.0
*/
public class ThrottleTask {
/**
* 節流實現關鍵類
*/
private Timer timer;
private Long delay;
private Runnable runnable;
private boolean needWait=false;
/**
* 有參建構函式
* @param runnable 要啟動的定時任務
* @param delay 延遲時間
*/
public ThrottleTask(Runnable runnable, Long delay) {
this.runnable = runnable;
this.delay = delay;
this.timer = new Timer();
}
/**
* build 建立物件,相當於 ThrottleTask task = new ThrottleTask();
* @param runnable 要執行的節流任務
* @param delay 延遲時間
* @return ThrottleTask 物件
*/
public static ThrottleTask build(Runnable runnable, Long delay){
return new ThrottleTask(runnable, delay);
}
public void taskRun(){
//如果 needWait 為 false,結果取反,表示式為 true。執行 if 語句
if(!needWait){
//設定為 true,這樣下次就不會再執行
needWait=true;
//執行節流方法
timer.schedule(new TimerTask() {
@Override
public void run() {
//執行完成,設定為 false,讓下次操作再進入 if 語句中
needWait=false;
//開啟多執行緒執行 run() 方法
runnable.run();
}
}, delay);
}
}
}
節流測試,每 2ms 請求一次,節流任務是每 1s 執行一次。真實效果應該是 1s 內前端發起了五次請求,但是後端只執行了一次操作
public static void main(String[] args){
//建立節流要執行的物件,並把要執行的任務傳入進去
ThrottleTask task = ThrottleTask.build(new Runnable() {
@Override
public void run() {
System.out.println("節流任務執行:do task: "+System.currentTimeMillis());
}
},1000L);
//while一直執行,模擬前端使用者一直請求後端
while (true){
System.out.println("前端請求後端:call task: "+System.currentTimeMillis());
task.taskRun();
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
前端請求後端:call task: 1659772459363
前端請求後端:call task: 1659772459574
前端請求後端:call task: 1659772459780
前端請求後端:call task: 1659772459995
前端請求後端:call task: 1659772460205
節流任務執行:do task: 1659772460377
前端請求後端:call task: 1659772460409
前端請求後端:call task: 1659772460610
前端請求後端:call task: 1659772460812
前端請求後端:call task: 1659772461027
前端請求後端:call task: 1659772461230
節流任務執行:do task: 1659772461417
idea 爆紅線了,強迫症的我受不了,肯定要解決它
腦子第一時間冒出來的是 @SuppressWarnings("all") 註解,跟所有的警告說拜拜~瞬間就清爽了
算了,壓制警告總感覺是不負責任。總不能這樣草草了事,那就來直面這個爆紅。既然讓我用 ScheduledExecutorService ,那簡單,直接替換
public class ThrottleTask {
/**
* 節流實現關鍵類:
*/
private ScheduledExecutorService timer;
private Long delay;
private Runnable runnable;
private boolean needWait=false;
/**
* 有參建構函式
* @param runnable 要啟動的定時任務
* @param delay 延遲時間
*/
public ThrottleTask(Runnable runnable, Long delay) {
this.runnable = runnable;
this.delay = delay;
this.timer = Executors.newSingleThreadScheduledExecutor();
}
/**
* build 建立物件,相當於 ThrottleTask task = new ThrottleTask();
* @param runnable 要執行的節流任務
* @param delay 延遲時間
* @return ThrottleTask 物件
*/
public static ThrottleTask build(Runnable runnable, Long delay){
return new ThrottleTask(runnable, delay);
}
public void taskRun(){
//如果 needWait 為 false,結果取反,表示式為 true。執行 if 語句
if(!needWait){
//設定為 true,這樣下次就不會再執行
needWait=true;
//執行節流方法
timer.schedule(new TimerTask() {
@Override
public void run() {
//執行完成,設定為 false,讓下次操作再進入 if 語句中
needWait=false;
//開啟多執行緒執行 run() 方法
runnable.run();
}
}, delay,TimeUnit.MILLISECONDS);
}
}
}
那麼定時器 Timer 和 ScheduledThreadPoolExecutor 解決方案之間的主要區別是什麼,我總結了三點...