wait函式_函式防抖:讓你的函式休息一下
技術標籤:wait函式回撥函式改變全域性變數
函式防抖,是指防止函式在極短的時間內反覆呼叫,造成資源的浪費
考慮一下電梯關門的場景,現代的大部分電梯都可以通過紅外,感知到是否有人進入,為了避免夾到人,同時為了等待後面的人,電梯關門的時間往往有這麼一種規則:始終保證電梯門在最後一個人進入後3秒後關閉。如果有人進入後,還沒有等到3秒又有人進來了,電梯門會以最後一次進入的時間為計時起點,重新等待3秒。
再考慮一個頁面上的場景,頁面上的某些事件觸發頻率非常高,比如滾動條滾動、視窗尺寸變化、滑鼠移動等,如果我們需要註冊這類事件,不得不考慮效率問題,又特別是事件處理中涉及到了大量的操作,比如:
window.onresize = function(){ //大量的dom操作}
當視窗尺寸發生變化時,哪怕只變化了一點點,都有可能造成成百上千次對處理函式的呼叫,這對網頁效能的影響是極其巨大的。於是,我們可以考慮,每次視窗尺寸變化、滾動條滾動、滑鼠移動時,不要立即執行相關操作,而是等一段時間,以視窗尺寸停止變化、滾動條不再滾動、滑鼠不再移動為計時起點,一段時間後再去執行操作,就像電梯關門那樣。
再考慮一個搜尋的場景(例如百度),當我在一個文字框中輸入文字(鍵盤按下事件)時,需要將文字傳送到伺服器,並從伺服器得到搜尋結果,這樣的話,使用者直接輸入搜尋文字就可以了,不用再去點搜尋按鈕,可以提升使用者體驗,類似於下面的效果:
上面的效果,我沒有點選搜尋按鈕,也沒有按回車鍵,只是寫了一些搜尋的文字而已。
可是如何來實現上面的場景呢?如果文字框的文字每次被改變(鍵盤按下事件),我都要把資料傳送到伺服器,得到搜尋結果,這是非常恐怖的!想想看,我搜索“google”這樣的單詞,至少需要按6次按鍵,就這一個詞,我需要向伺服器請求6次,並讓伺服器去搜索6次,但我只需要最後一次的結果就可以了。如果考慮使用者按錯的情況,傳送請求的次數更加恐怖。這樣就造成了大量的頻寬被佔用,浪費了很多資源。
如何避免這樣的問題呢?你再仔細看看百度的處理方式:
你會發現,真正的搜尋行為,並不是每次按鍵都會觸發的,只有當用戶停止按鍵一段事件後才會觸發。
於是,為了滿足這種型別場景,我們可以開發一個通用的函式,這個函式要滿足以下功能:
- 呼叫該函式後,不立即做事,而是一段時間後去做事
- 如果在等待時間內呼叫了該函式,重新計時
這樣的功能,就叫做函式防抖,其實就是防止函式短時間內被呼叫多次。要完成該函式,需要給予兩個條件:
- 告訴我一段時間後要做什麼事(這裡應該是一個回撥函式,即函式作為引數)
- 告訴我要等待多長時間(毫秒)
基於這樣的理解,我們可以編寫如下的程式碼:
//設定全域性變數,記錄setTimeout得到的idlet timerId = null;/**
* 函式防抖
* @param {function} func 一段時間後,要呼叫的函式
* @param {number} wait 等待的時間,單位毫秒
*/function debounce(func, wait){ if(timerId){ //如果有值,說明目前正在等待中,清除它
clearTimeout(timerId);
} //重新開始計時
timerId = setTimeout(() => {
func();
}, wait);
}
現在來測試一下這個函式:
<input type="text" id="txt"><script>
document.getElementById("txt").onkeyup = (event)=>{ //500毫秒內沒有再次呼叫debounce,才會真正執行回撥函式
debounce(()=>{ console.log(event.target.value);
}, 500);
}</script>
效果如下:
雖然我們實現了效果,可是仔細想想,目前這個防抖函式還是太年輕,太簡單,有時候還很幼稚。
因為它使用了一個全域性變數timerId
,這意味著,頁面上所有需要防抖的地方都要共用這個變數,如果我同時要對視窗尺寸改變事件和文字框按鍵事件進行防抖,就辦不到了。
於是,我決定把這個變數寫到函式裡面去:
/**
* 函式防抖
* @param {function} func 一段時間後,要呼叫的函式
* @param {number} wait 等待的時間,單位毫秒
*/function debounce(func, wait){ //設定變數,記錄setTimeout得到的id
let timerId = null; if(timerId){ //如果有值,說明目前正在等待中,清除它
clearTimeout(timerId);
} //重新開始計時
timerId = setTimeout(() => {
func();
}, wait);
}
但是這樣子還是有問題,這樣的話,每次呼叫防抖函式都會得到一個新的計時器,過去的計時器找不到了。
為了解決這個問題,我決定把程式碼改造成以下結構:
/**
* 函式防抖
* @param {function} func 一段時間後,要呼叫的函式
* @param {number} wait 等待的時間,單位毫秒
*/function debounce(func, wait){ //設定變數,記錄setTimeout得到的id
let timerId = null; return function(){ if(timerId){ //如果有值,說明目前正在等待中,清除它
clearTimeout(timerId);
} //重新開始計時
timerId = setTimeout(() => {
func();
}, wait);
}
}
這樣一來,debounce
函式僅僅負責生成一個timerId
,保證每次呼叫debounce
都會有一個新的timerId
產生即可,將防抖的工作,交給了返回的函式去完成。今後如果我要對某一個操作進行防抖,我只需要呼叫debounce
函式來得到一個操作函式,後面反覆去呼叫操作函式即可。
下面是測試的程式碼:
let txtChange = debounce(() => { console.log(document.getElementById("txt").value);
}, 500);document.getElementById("txt").onkeyup = () => {
txtChange();
}let winChange = debounce(() => { console.log("視窗尺寸改變了");
}, 500);window.onresize = () => {
winChange();
}
下面是執行效果:
目前,我們的防抖函式還無法傳遞引數給回撥函式,只要稍稍做一點改變,即可實現引數的傳遞:
/**
* 函式防抖
* @param {function} func 一段時間後,要呼叫的函式
* @param {number} wait 等待的時間,單位毫秒
*/function debounce(func, wait){ //設定變數,記錄setTimeout得到的id
let timerId = null; return function(...args){ if(timerId){ //如果有值,說明目前正在等待中,清除它
clearTimeout(timerId);
} //重新開始計時
timerId = setTimeout(() => {
func(...args);
}, wait);
}
}
之後,我們就可以在呼叫時傳遞引數了:
let txtChange = debounce((event) => { console.log(event.target.value);
}, 500);document.getElementById("txt").onkeyup = (event) => {
txtChange(event); //將引數傳遞給回撥函式}
有了防抖函式,我們將來遇到滑鼠移動、視窗尺寸改變等需要大量呼叫同一個函式的時候,就可以使用它來降低執行頻率,保證執行效率。