上一篇文章講了去抖函式,然後這一篇講同樣為了優化效能,降低事件處理頻率的節流函式。
一、什麼是節流?
節流函式(throttle)就是讓事件處理函式(handler)在大於等於執行週期時才能執行,週期之內不執行,即事件一直被觸發,那麼事件將會按每小段固定時間一次的頻率執行。
打個比方:王者榮耀、英雄聯盟、植物大戰殭屍遊戲中,技能的冷卻時間,技能的冷卻過程中,是無法使用技能的,只能等冷卻時間到之後才能執行。
那什麼樣的場景能用到節流函式呢?
比如:
- 頁面滾動和改變大小時需要進行業務處理,比如判斷是否滑到底部,然後進行懶載入資料。
- 按鈕被高頻率地點選時,比如遊戲和搶購網站。
我們通過一個簡單的示意來理解:
節流函式可以用時間戳和定時器兩種方式進行處理。
二、時間戳方式實現
<div class="container" id="container">
正在滑動:0
</div>
<script>
window.onload = function() {
var bodyEl = document.getElementsByTagName("body")[0]
}
var count = 0;
window.onmousemove = throttle(eventHandler, 1000);
function eventHandler(e) {
var containerEl = document.getElementById("container");
containerEl.innerHTML = "正在滑動: " + count;
count++;
}
function throttle(func, delay) {
var delay = delay || 1000;
var previousDate = new Date();
var previous = previousDate.getTime(); // 初始化一個時間,也作為高頻率事件判斷事件間隔的變數,通過閉包進行儲存。
return function(args) {
var context = this;
var nowDate = new Date();
var now = nowDate.getTime();
if (now - previous >= delay) { // 如果本次觸發和上次觸發的時間間隔超過設定的時間
func.call(context, args); // 就執行事件處理函式 (eventHandler)
previous = now; // 然後將本次的觸發時間,作為下次觸發事件的參考時間。
}
}
}
</script>
複製程式碼
看時間戳實現版本的效果:
三、定時器方式實現
<div class="container" id="container">
正在滑動: 0
</div>
<script>
window.onload = function() {
var bodyEl = document.getElementsByTagName("body")[0]
}
var count = 0;
window.onmousemove = throttle(eventHandler, 1000);
function eventHandler(e) {
var containerEl = document.getElementById("container");
containerEl.innerHTML = "正在滑動: " + count;
count++;
}
function throttle(func, delay) {
var delay = delay || 1000;
var timer = null;
return function(args) {
var context = this;
var nowDate = new Date();
var now = nowDate.getTime();
if (!timer) {
timer = setTimeout(function() {
func.call(context, args);
timer = null;
}, delay)
}
}
}
</script>
複製程式碼
看看定時器版實現版本的效果:
三、時間戳和定時器的對比分析
對比時間戳和定時器兩種方式,效果上的區別主要在於:
事件戳方式會立即執行,定時器會在事件觸發後延遲執行,而且事件停止觸發後還會再延遲執行一次。
具體選擇哪種方式取決於使用場景。underscore 把這兩類場景用 leading 和 trailing 進行了表示。
四、underscore 原始碼實現
underscore 的原始碼中就同時實現了時間戳和定時器實現方式,在呼叫時可以自由選擇要不要在間隔時間開始時(leading)執行,或是間隔時間結束後(trailing)執行。
具體看虛擬碼和示意圖:
<div class="container" id="container">
正在滑動: 0
</div>
<div class="height"></div>
<script>
window.onload = function() {
var bodyEl = document.getElementsByTagName("body")[0]
}
var count = 0;
// 事件處理函式
function eventHandler(e) {
var containerEl = document.getElementById("container");
containerEl.innerHTML = "正在滑動: " + count;
count++;
}
var _throttle = function(func, wait, options) {
var context, args, result;
// 定時器變數預設為 null, 是為了如果想要觸發了一次後再延遲執行一次。
var timeout = null;
// 上一次觸發事件回撥的時間戳。 預設為 0 是為了方便第一次觸發預設立即執行
var previous = 0;
// 如果沒有傳入 options 引數
// 則將 options 引數置為空物件
if (!options)
options = {};
var later = function() {
// 如果 options.leading === false
// 則每次觸發回撥後將 previous 置為 0, 表示下次事件觸發會立即執行事件處理函式
// 否則置為當前時間戳
previous = options.leading === false ? 0 : +new Date();
// 剩餘時間跑完,執行事件,並把定時器變數置為空,如果不為空,那麼剩餘時間內是不會執行事件處理函式的,見 else if 那。
timeout = null;
result = func.apply(context, args);
// 剩餘時間結束,並執行完事件後,清理閉包中自由變數的記憶體垃圾,因為不再需要了。
if (!timeout)
context = args = null;
};
// 返回的事件回撥函式
return function() {
// 記錄當前時間戳
var now = +new Date();
// 第一次執行回撥(此時 previous 為 0,之後 previous 值為上一次時間戳)
// 並且如果程式設定第一個回撥不是立即執行的(options.leading === false)
// 則將 previous 值(表示上次執行的時間戳)設為 now 的時間戳(第一次觸發時)
// 表示剛執行過,這次就不用執行了
if (!previous && options.leading === false)
previous = now;
// 間隔時間 和 上一次到本次事件觸發回撥的持續時間的時間差
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 如果間隔時間還沒跑完,則不會執行任何事件處理函式。
// 如果超過間隔時間,就可以觸發方法(remaining <= 0)
// remaining > wait,表示客戶端系統時間被調整過
// 也會立即執行 func 函式
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
// 解除引用,防止記憶體洩露
timeout = null;
}
// 重置前一次觸發的時間戳
previous = now;
// result 為事件處理函式(handler)的返回值
// 採用 apply 傳遞類陣列物件 arguments
result = func.apply(context, args);
// 引用置為空,防止記憶體洩露
if (!timeout)
context = args = null;
} else if (!timeout && options.trailing !== false) {
// 如果 remaining > 0, 表示在間隔時間內,又觸發了一次事件
// 如果 trailing 為真,則會在間隔時間結束時執行一次事件處理函式(handler)
// 在從觸發到剩餘時間跑完,會利用一個定時器執行事件處理函式,並在定時器結束時把 定時器變數置為空
// 如果剩餘事件內已經存在一個定時器,則不會進入本 else if 分支, 表示剩餘時間已經有一個定時器在執行,該定時器會在剩餘時間跑完後執行。
// 如果 trailing = false,即不需要在剩餘時間跑完執行事件處理函式。
// 間隔 remaining milliseconds 後觸發 later 方法
timeout = setTimeout(later, remaining);
}
// 回撥返回值
return result;
};
};
window.onmousemove = _throttle(eventHandler, 1000);
</script>
複製程式碼
下面是我畫的示意圖:
大致總結一下程式碼對事件處理邏輯的影響:- 如果 leading 為真,那麼綠色意味著間隔時間的開始會立即執行,第一次觸發也會立即執行。
- 如果 trailing 為真,那麼從藍紫色的豎細線後的剩餘事件,會跑一個定時器,定時器在時間間隔結束時再執行一次事件處理函式。
- 如果 leading 不為真,那麼第一次事件觸發不會立即執行。
- 如果 trailing 不為真,最後一次事件觸發後,不然再執行一次事件處理函式。
節流和去抖的常見場景
- 輸入框打字輸入完後才開始非同步請求資料校驗內容:去抖
- 下拉滾動條判斷是否到底部,進行懶載入資料:去抖和節流都可以,判斷是否到底的方式不同
- 活動網站、遊戲網站,按鈕被瘋狂點選:節流
五、總結
去抖和節流函式都是為了降低高頻率事件觸發的事件處理頻率,從而優化網頁中大量重繪重排帶來的效能問題。
其區別在於去抖會在高頻率事件觸發時,只執行一次,節流會在滿足間隔時間後執行一次。去抖的 immediate,節流中的 leading, trailing 都是為了儘可能滿足這類工具函式的不同使用場景。
歡迎關注我的個人公眾號“謝南波”,專注分享原創文章。