前言
在前端開發過程中,我們會遇到一些頻繁觸發的事件,但我們需要控制回調的頻率,比如下面幾種場景:
- 遊戲中的按鍵響應,比如格鬥,比如射擊,需要控制出拳和射擊的速率。
- 自動完成,按照一定頻率分析輸入,提示自動完成。
- 鼠標移動和窗口滾動,鼠標稍微移動一下,窗口稍微滾動一下會帶來大量的事件,因而需要控制回調的發生頻率。
下面我們通過代碼來看看mousemove事件是如何頻繁觸發的:
index.html文件代碼如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>debounce防抖</title>
<style>
#container{
width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
}
</style>
</head>
<body>
<div id="container"></div>
<script src="debounce.js"></script>
<script>
var count = 1;
var container = document.getElementById("container");
function mouseMove() {
console.log(this);
container.innerHTML = count++;
};
container.onmousemove = mouseMove
</script>
</body>
</html>
運行該html文件,我們將鼠標在我們定義的矩形區域移動,只是簡單的從下往上滑動,mouseMove函數就被觸發了99次。
假設mouseMove函數時複雜的回調函數或者是ajax請求,如果我們沒有對事件處理函數調用的頻率進行限制,會加重瀏覽器的負擔,導致用戶體驗極差。這時候我們可以採用debounce(防抖)或throttle(節流)的方式來減少調用頻率,同時又不影響實際效果。
今天我們主要講講防抖。
歡迎關注我的微信公衆號:前端極客技術(FrontGeek)
防抖
原理
函數防抖(debounce):在事件被觸發n秒後再執行回調,如果在這n秒內又被觸發,則重新計時。
初步實現
根據上面防抖的描述,我們可以用setTimeout寫第一版防抖函數的實現代碼:
function debounce(func, wait) {
var timeout
return function () {
clearTimeout(timeout)
timeout = setTimeout(func, wait)
}
}
在最開始的例子中使用debounce:
container.onmousemove = debounce(mouseMove, 1000)
這樣子修改後,只有在我們移動完1s內不再觸發,纔會執行回調事件mouseMove。
this指向問題
我們在mouseMove函數中執行 console.log(this),會發現不使用debounce和使用debounce情況下,this的值是不一樣的。
不使用debounce時this的值爲
<div id="container"></div>
而使用debounce函數時,this則會指向window對象。
所以我們需要將this指向正確的函數,這時候我們可以利用apply()方法實現。代碼修改如下:
function debounce(func, wait) {
var timeout
return function () {
var context = this
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context)
}, wait)
}
}
修改完成後,我們再觸發事件,可以看到此時this的指向正確了。
event對象
JavaScript在事件處理函數中會提供事件對象event,我們將mouseMove函數修改如下:
function mouseMove(e) {
console.log(this);
console.log(e);
container.innerHTML = count++;
};
如果不使用debounce函數,控制檯打印的e是MouseEvent對象,但當我們調用debounce函數,打印出來的卻是undefined
。
所以我們再次修改代碼如下:
function debounce(func, wait) {
var timeout
return function () {
var context = this
var args = arguments
clearTimeout(timeout)
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
}
至此,我們修復了this指向和event對象問題,整個防抖函數已經算是比較完善了。
立即執行
接下來我們再考慮一個新的需求:如果我們希望是在事件一觸發就立刻執行函數,而不是等到事件停止觸發後再執行;並且等到停止觸發n秒後,纔可以重新觸發執行。
我們可以通過immediate參數來判斷是否立刻執行,代碼修改如下:
function debounce(func, wait, immediate) {
var timeout
return function () {
var context = this
var args = arguments
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
// 已經執行過不再執行
var callNow = !timeout
timeout = setTimeout(function() {
timeout = null
}, wait)
if (callNow) {
func.apply(context, args)
}
} else {
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
}
}
container.onmousemove = debounce(mouseMove, 1000, true)
返回值
我們需要注意的一點是:mouseMove函數可能是有返回值的,所以我們也要返回函數的執行結果,但是immediate爲false的時候,因爲使用setTimeout,我們將func.apply(context, args)的返回值賦給變量,最後再return的時候,值會一直是undefined,所以我們只在immediate爲true的時候返回函數的執行結果。
function debounce(func, wait, immediate) {
var timeout, result
return function () {
var context = this
var args = arguments
if (timeout) {
clearTimeout(timeout)
}
if (immediate) {
// 已經執行過不再執行
var callNow = !timeout
timeout = setTimeout(function() {
timeout = null
}, wait)
if (callNow) {
result = func.apply(context, args)
}
} else {
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
return result
}
}
取消
假設防抖的時間間隔爲10秒,immediate爲true的情況下,只有等10秒後才能重新觸發事件,這時候我希望有個按鈕可以取消防抖,這樣我再去觸發時,又可以立即執行了。
下面我們來實現這個取消功能:
function debounce(func, wait, immediate) {
var timeout, result
var debounced = function () {
var context = this
var args = arguments
// 每次新的嘗試調用func,會使拋棄之前等待的func
if (timeout) clearTimeout(timeout)
// 如果允許新的調用嘗試立即執行
if (immediate) {
// 如果之前尚沒有調用嘗試,那麼此次調用可以立馬執行,否則就需要等待
var callNow = !timeout
// 刷新timeout
timeout = setTimeout(function() {
timeout = null
}, wait)
// 如果能被立即執行,立即執行
if (callNow) result = func.apply(context, args)
} else {
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
return result
}
debounced.cancel = function () {
clearTimeout(timeout)
timeout = null
}
return debounced
}
如何調用這個cancel函數?
var setMouseMove = debounce(mouseMove, 10000, true)
container.onmousemove = setMouseMove
// buttonClick爲button的click事件
function buttonClick() {
setMouseMove.cancel()
}
效果如下:
到這裏,一個完整的debounce函數已經實現了。
總結
debounce防抖函數,滿足的是:高頻下只響應一次。
在實際開發過程中,常見的應用場景有:
- 在輸入框快速輸入文字(高頻),我們只想在其完全停止輸入時再對輸入文字做處理(一次)
- ajax,大多數場景下,每個異步請求在短時間內只能響應一次,比如下拉刷新、不停地上拉加載,但只發送一次ajax請求。
歡迎關注我的微信公衆號:【前端極客技術】