JavaScript 專題系列第二篇,講解節流,帶你從零實現一個 underscore 的 throttle 函式
前言
在《JavaScript專題之跟著underscore學防抖》中,我們瞭解了為什麼要限制事件的頻繁觸發,以及如何做限制:
- debounce 防抖
- throttle 節流
今天重點講講節流的實現。
節流
節流的原理很簡單:
如果你持續觸發事件,每隔一段時間,只執行一次事件。
根據首次是否執行以及結束後是否執行,效果有所不同,實現的方式也有所不同。
我們用 leading 代表首次是否執行,trailing 代表結束後是否再執行一次。
關於節流的實現,有兩種主流的實現方式,一種是使用時間戳,一種是設定定時器。
使用時間戳
讓我們來看第一種方法:使用時間戳,當觸發事件的時候,我們取出當前的時間戳,然後減去之前的時間戳(最一開始值設為 0 ),如果大於設定的時間週期,就執行函式,然後更新時間戳為當前的時間戳,如果小於,就不執行。
看了這個表述,是不是感覺已經可以寫出程式碼了…… 讓我們來寫第一版的程式碼:
// 第一版
function throttle(func, wait) {
var context, args;
var previous = 0;
return function() {
var now = +new Date();
context = this;
args = arguments;
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
}
}複製程式碼
例子依然是用講 debounce 中的例子,如果你要使用:
container.onmousemove = throttle(getUserAction, 1000);複製程式碼
效果演示如下:
我們可以看到:當滑鼠移入的時候,事件立刻執行,每過 1s 會執行一次,如果在 4.2s 停止觸發,以後不會再執行事件。
使用定時器
接下來,我們講講第二種實現方式,使用定時器。
當觸發事件的時候,我們設定一個定時器,再觸發事件的時候,如果定時器存在,就不執行,直到定時器執行,然後執行函式,清空定時器,這樣就可以設定下個定時器。
// 第二版
function throttle(func, wait) {
var timeout;
var previous = 0;
return function() {
context = this;
args = arguments;
if (!timeout) {
timeout = setTimeout(function(){
timeout = null;
func.apply(context, args)
}, wait)
}
}
}複製程式碼
為了讓效果更加明顯,我們設定 wait 的時間為 3s,效果演示如下:
我們可以看到:當滑鼠移入的時候,事件不會立刻執行,晃了 3s 後終於執行了一次,此後每 3s 執行一次,當數字顯示為 3 的時候,立刻移出滑鼠,相當於大約 9.2s 的時候停止觸發,但是依然會在第 12s 的時候執行一次事件。
所以比較兩個方法:
- 第一種事件會立刻執行,第二種事件會在 n 秒後第一次執行
- 第一種事件停止觸發後沒有辦法再執行事件,第二種事件停止觸發後依然會再執行一次事件
雙劍合璧
那我們想要一個什麼樣的呢?
有人就說了:我想要一個有頭有尾的!就是滑鼠移入能立刻執行,停止觸發的時候還能再執行一次!
所以我們綜合兩者的優勢,然後雙劍合璧,寫一版程式碼:
// 第三版
function throttle(func, wait) {
var timeout, context, args, result;
var previous = 0;
var later = function() {
previous = +new Date();
timeout = null;
func.apply(context, args)
};
var throttled = function() {
var now = +new Date();
//下次觸發 func 剩餘的時間
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果沒有剩餘的時間了或者你改了系統時間
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
timeout = setTimeout(later, remaining);
}
};
return throttled;
}複製程式碼
效果演示如下:
我們可以看到:滑鼠移入,事件立刻執行,晃了 3s,事件再一次執行,當數字變成 3 的時候,也就是 6s 後,我們立刻移出滑鼠,停止觸發事件,9s 的時候,依然會再執行一次事件。
優化
但是我有時也希望無頭有尾,或者有頭無尾,這個咋辦?
那我們設定個 options 作為第三個引數,然後根據傳的值判斷到底哪種效果,我們約定:
leading:false 表示禁用第一次執行
trailing: false 表示禁用停止觸發的回撥
我們來改一下程式碼:
// 第四版
function throttle(func, wait, 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 = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
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;
}複製程式碼
取消
在 debounce 的實現中,我們加了一個 cancel 方法,throttle 我們也加個 cancel 方法:
// 第五版 非完整程式碼,完整程式碼請檢視最後的演示程式碼連結
...
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
...複製程式碼
注意
我們要注意 underscore 的實現中有這樣一個問題:
那就是 leading:false
和 trailing: false
不能同時設定。
如果同時設定的話,比如當你將滑鼠移出的時候,因為 trailing 設定為 false,停止觸發的時候不會設定定時器,所以只要再過了設定的時間,再移入的話,就會立刻執行,就違反了 leading: false,bug 就出來了,所以,這個 throttle 只有三種用法:
container.onmousemove = throttle(getUserAction, 1000);
container.onmousemove = throttle(getUserAction, 1000, {
leading: false
});
container.onmousemove = throttle(getUserAction, 1000, {
trailing: false
});複製程式碼
至此我們已經完整實現了一個 underscore 中的 throttle 函式,恭喜,撒花!
演示程式碼
相關的程式碼可以在 Github 部落格倉庫 中找到
專題系列
JavaScript專題系列目錄地址:github.com/mqyqingfeng…。
JavaScript專題系列預計寫二十篇左右,主要研究日常開發中一些功能點的實現,比如防抖、節流、去重、型別判斷、拷貝、最值、扁平、柯里、遞迴、亂序、排序等,特點是研(chao)究(xi) underscore 和 jQuery 的實現方式。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。