在上一篇文章 JavaScript-函數防抖 中我們學習了什麼是防抖,並且一步步實現了防抖函數,今天我們一起來學習節流(throttle)。
什麼是節流
函數節流(throttle):當持續觸發事件時,保證一定時間段內只調用一次事件處理函數。簡單的說,就是讓一個函數無法在很短時間間隔內被連續調用,只有當上一次函數執行後過了規定的時間間隔,才能進行下一次函數的執行。
函數節流主要有兩種實現方法:時間戳和定時器。
歡迎關注我的微信公衆號:前端極客技術(FrontGeek)
節流的實現
時間戳
思路
只要觸發,就用Date方法獲取當前時間 now,與上一次調用的時間 previous 作比較
-
如果時間差大於等於規定的時間間隔,就執行一次目標函數,執行以後,將存儲上一次調用時間previous的值更新爲當前時間now
-
如果時間差小於規定的時間間隔,則等待下一次觸發重新進行第一步操作。
代碼實現
// delay:規定的時間間隔
function throttle(func, delay) {
var context, args
var previous = 0
return function() {
context = this
args = arguments
var now = +new Date()
if (now - previous >= delay) {
func.apply(context, args)
previous = now
}
}
}
我們依舊採用防抖那篇文章用到的鼠標移動的例子來驗證節流,調用節流函數方式如下:
container.onmousemove = throttle(mouseMove, 2000)
效果如下:
從上圖中可以看到:當鼠標移入時,事件立即執行,每過2秒會執行一次,假設在第7秒時移出,停止觸發,以後不會再執行事件。
定時器
思路
用定時器實現時間間隔。
- 當定時器不存在,說明可以執行函數,定義一個定時器來向任務隊列註冊目標函數。目標函數執行後設置保存定時器ID變量爲空
- 當定時器已經被定義,說明已經在等待過程中,則等待下次觸發事件時再進行查看。
代碼實現
function throttle(func, delay) {
var timeout = null
var context, args
return function() {
context = this
args = arguments
if (!timeout) {
timeout = setTimeout(function() {
timeout = null
func.apply(context, args)
}, delay)
}
}
}
代碼執行效果如下:
從上面的動圖我們可以看到:鼠標移入時,事件不會立即執行,之後每隔2秒執行一次,假設在第5秒時移出,事件停止觸發,但在第6秒時依舊會執行一次事件。
兩者區別:
- 時間戳實現:觸發事件一發生先執行目標函數,然後再等待規定的時間間隔再次執行目標函數。如果在等待過程中停止觸發,後續不會再執行目標函數。
- 定時器實現:觸發事件一發生,先等待夠規定的時間間隔再執行目標函數。即使在等待過程中停止觸發,若定時器已經在任務隊列裏註冊了定時器,也會執行最後一次。
強強聯合:時間戳+定時器
如果我們想要能夠控制鼠標移入能夠立即執行,停止觸發的時候能夠再執行一次,我們可以綜合時間戳和定時器兩種方法來實現“有頭有尾”的效果。
在這裏我們需要注意:控制好在上一週期的“尾”和下一週期的“頭”之間時間間隔,我們引入變量remaining表示還需要等待的時間,來讓尾部那一次的執行也符合時間間隔。
代碼實現
function throttle(func, delay) {
var timeout, context, args, result
var previous = 0
var throttled = function() {
context = this
args = arguments
var now = +new Date()
// 下次觸發func剩餘時間
var remaining = delay - (now - previous)
// 如果沒有剩餘的時間了或者你改了系統時間
if (remaining <= 0 || remaining > delay) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
func.apply(context, args)
previous = now
} else if (!timeout) {
timeout = setTimeout(function(){
previous = +new Date()
timeout = null
func.apply(context, args)
}, remaining)
}
}
return throttled
}
代碼執行效果如下:
優化
在上面結合時間戳和定時器的解法的基礎上,如果我們想實現是否啓用第一次 / 尾部最後一次計時回調的執行,如何實現?
我們可以設置個options作爲第三個參數,然後根據傳的值判斷到底哪種效果,我們約定:
- leading:false表示禁用第一次執行
- trailing:false表示禁用停止觸發的回調
代碼實現如下:
function throttle(func, delay, options) {
var timeout, context, args, result
var previous = 0
if (!options) options = {}
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime()
timeout = null
func.apply(context, args)
if (!timeout) context = args = null
}
var throttled = function() {
var now = new Date().getTime()
if (!previous && options.leading === false) previous = now
var remaining = delay - (now - previous)
context = this
args = arguments
if (remaining <= 0 || remaining > delay) {
if (timeout) {
clearTimeout(timeout)
timeout = null
}
previous = now
func.apply(context, args)
if (!timeout) context = args = null
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining)
}
}
return throttled
}
我們想要第一次立即執行,並且禁用停止觸發的回調,調用throttle方法如下:
container.onmousemove = throttle(mouseMove, 2000, {leading: true, trailing: false})
效果如下圖:
取消
在防抖函數中,我們實現了cancel方法,在節流函數中,同理:
function throttle(func, delay, options) {
.....
var throttled = function() {
....
}
throttled.cancel = function() {
clearTimeout(timeout)
previous = 0
timeout = null
}
return throttled
}
存在的問題
至此,一個完整的節流函數已經實現好了,但是仍然存在一個問題:就是 leading:false 和 trailing: false 不能同時設置。
如果同時設置的話,比如當你將鼠標移出的時候,因爲 trailing 設置爲 false,停止觸發的時候不會設置定時器,所以只要再過了設置的時間,再移入的話,就會立刻執行,就違反了 leading: false,bug 就出來了。如下圖所示:
總結
防抖和節流的作用都是防止函數多次調用。區別在於:假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於wait,防抖的情況下只會調用一次,而節流的情況會每隔一段時間wait調用函數。
相比 debounce,throttle 要更加寬鬆一些,其目的在於:按頻率執行調用。
參考來源:https://github.com/mqyqingfeng/Blog/issues/26
歡迎關注我的微信公衆號:前端極客技術(FrontGeek)