前言
js的典型的場景
- 監聽頁面的scroll事件
- 拖拽事件
- 監聽滑鼠的 mousemove 事件
...
這些事件會頻繁觸發會影響效能,如果使用節流,降低頻次,保留了使用者體驗,又提升了執行速度,節省資源。
原理
節流的原理:持續觸發某事件,每隔一段時間,只執行一次。
通俗點說,3 秒內多次呼叫函式,但是在 3 秒間隔內只執行一次,第一次執行後 3 秒 無視後面所有的函式呼叫請求,也不會延長時間間隔。3 秒間隔結束後則開始執行新的函式呼叫請求,然後在這新的 3 秒內依舊無視後面所有的函式呼叫請求,以此類推。
簡單來說:每隔單位時間( 3 秒),只執行一次。
實現方式
目前比較主流的實現方式有兩種:時間戳、定時器。
時間戳實現
使用時間戳實現:首先初始化執行事件的時間previous為0,然後將當前的時間戳減去上次執行時間(now - previous),如果大於wait,則直接執行函式,並且將此時的執行時間now賦給previous(previous = now)。
由於首次previous = 0,則此時函式第一次觸發就會立即執行。
後續則每隔wait時間執行一次,如果停止觸發,則不會再執行函式。
// 由於一開始now - 0 > wait,則這個寫法,時間會立即執行,沒過一秒會執行一次,停止觸發,則不會再執行事件
function throttle(func, wait = 500) {
let context, now;
let previous = 0; // 設定過去的執行時間初始值為0
return function (...args) {
context = this;
now = +(Date.now() || new Date().getTime());
if (now - previous > wait) {
func.apply(context, args);
previous = now;
}
};
}
定時器實現
使用定時器實現:首先初始化timeout,然後定義!timeout為true的情況下,直接執行setTimeout,,等待wait時間後執行函式,然後清空timeout,以此類推,重新進入也會按上述執行。
由於進入函式,就執行setTimeout,所以不會立即觸發函式執行。
後續則每隔wait時間執行一次,如果停止觸發,而後還會觸發執行一次函式。
// 由於一進入就建立了定時器,所以不會立即觸發函式執行
function throttle(func, wait = 500) {
let context, timeout;
return function (...args) {
context = this;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
合併版本
如果,我們需要既剛開始就立即執行,停止觸發後,還會觸發執行一次函式。
下面,我們將定時器和時間戳合併,組成一個全新的節流版本。
function throttle(func, wait = 500) {
let context, timeout, result;
let previous = 0;
const throttled = function (...args) {
context = this;
const now = +(Date.now() || new Date().getTime()); // 當前時間
// 下次觸發 func 剩餘時間
const remaining = wait - (now - previous);
// 如果沒有剩餘時間或者改了系統時間,這時候不需要等待,直接立即執行,這樣就會第一次就執行
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func.apply(context, args);
} else if (!timeout) {
// 剩餘的情況就是remaining<=wait的情況,這裡使用setTimeout就可以最後也會執行一次
timeout = setTimeout(function () {
timeout = null;
previous = +(Date.now() || new Date().getTime()); // 這裡是將previous重新賦值當前時間
func.apply(context, args);
}, remaining);
}
};
return throttled;
}
合併版本優化
由於合併後的版本並沒用返回值的優化+取消功能。
下面對程式碼進行返回值+取消功能優化:
function throttle(func, wait = 500) {
let context, timeout, result;
let previous = 0;
const showResult = function (e1, e2) {
result = func.apply(e1, e2);
return result;
};
const throttled = function (...args) {
context = this;
const now = +(Date.now() || new Date().getTime()); // 當前時間
// 下次觸發 func 剩餘時間
const remaining = wait - (now - previous);
// 如果沒有剩餘時間或者改了系統時間,這時候不需要等待,直接立即執行,這樣就會第一次就執行
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
return showResult(context, args);
} else if (!timeout) {
// 剩餘的情況就是remaining<=wait的情況,這裡使用setTimeout就可以最後也會執行一次
timeout = setTimeout(function () {
timeout = null;
previous = +(Date.now() || new Date().getTime()); // 這裡是將previous重新賦值當前時間
return showResult(context, args);
}, remaining);
}
retrun result
};
throttled.cancel = function () {
if (timeout !== undefined) {
clearTimeout(timeout);
}
previous = 0;
context = timeout = result = undefined;
};
return throttled;
}
功能性優化
有時候,我們也希望無頭有尾,或者有頭無尾。
function throttle(func, wait = 500, options = {}) {
let context, timeout, result;
let previous = 0;
// 如果同時設定無頭無尾,則直接使用預設設定,其他情況,則走下述操作
if (!(options.leading === false && options.trailing === false)) {
leading = !!options.leading; // 預設去除立即執行部分
trailing = "trailing" in options ? !!options.trailing : true; // 預設保留尾部
}
// 返回原函式的return
const showResult = function (e1, e2) {
result = func.apply(e1, e2);
return result;
};
// 獲取當前時間
const getNow = function () {
return +(Date.now() || new Date().getTime());
};
const throttled = function (...args) {
context = this;
const now = getNow(); // 當前時間
// 下次觸發 func 剩餘時間
if (!previous && leading === false) previous = now;
const remaining = wait - (now - previous);
// 如果沒有剩餘時間或者改了系統時間,這時候不需要等待,直接立即執行,這樣就會第一次就執行
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
return showResult(context, args);
} else if (!timeout && trailing !== false) {
// 剩餘的情況就是remaining<=wait的情況,這裡使用setTimeout就可以最後也會執行一次
timeout = setTimeout(function () {
timeout = null;
previous = options.leading === false ? 0 : getNow(); // 這裡是將previous重新賦值當前時間
return showResult(context, args);
}, remaining);
}
return result;
};
throttled.cancel = function () {
if (timeout !== undefined) {
clearTimeout(timeout);
}
previous = 0;
context = timeout = result = undefined;
};
return throttled;
}
這裡,如果options不傳引數,函式預設設定
let leading = false
let trailing = true
也就是無頭有尾。
如果同時設定無頭無尾,則會直接採用預設設定,無頭有尾。
// 如果同時設定無頭無尾,則直接使用預設設定,其他情況,則走下述操作
if (!(options.leading === false && options.trailing === false)) {
leading = !!options.leading; // 預設去除立即執行部分
trailing = "trailing" in options ? !!options.trailing : true; // 預設保留尾部
}
演示地址
可以去Github倉庫檢視演示程式碼
跟著大佬學系列
主要是日常對每個進階知識點的摸透,跟著大佬一起去深入瞭解JavaScript的語言藝術。
後續會一直更新,希望各位看官不要吝嗇手中的贊。
❤️ 感謝各位的支援!!!
❤️ 如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!!!
❤️ 喜歡或者有所啟發,歡迎 star!!!