JavaScript-函數防抖

前言

在前端開發過程中,我們會遇到一些頻繁觸發的事件,但我們需要控制回調的頻率,比如下面幾種場景:

  • 遊戲中的按鍵響應,比如格鬥,比如射擊,需要控制出拳和射擊的速率。
  • 自動完成,按照一定頻率分析輸入,提示自動完成。
  • 鼠標移動和窗口滾動,鼠標稍微移動一下,窗口稍微滾動一下會帶來大量的事件,因而需要控制回調的發生頻率。

下面我們通過代碼來看看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請求。

歡迎關注我的微信公衆號:【前端極客技術】
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章