Javascript 中的函數大多數狀況下都是用戶調用執行的,可是在某些場景下不是用戶直接控制的,在這些場景下,函數會被頻繁調用,容易形成性能問題。html
好比在 window.onresize
事件和 window.onScroll
事件中,因爲用戶能夠不斷地觸發,這會致使函數短期內頻繁調用,若是函數中有複雜的計算,很容易就形成性能的問題。前端
這些場景下最主要的問題是觸發頻率過高,1s內能夠觸發數次,可是大多數狀況下咱們並不須要那麼高的觸發頻率,可能只要在500ms內觸發一次,這樣其實咱們能夠用 setTimeout
來解決,在這期間的觸發都忽略掉。git
咱們能夠先嚐試着本身實現一個節流函數:github
// 本身實現的簡單節流函數
function throttle (func, time) {
var timeout = null,
context = null,
args = null
return function() {
context = this
args = arguments
// 只要timeout函數存在,全部調用都無視
if(timeout) return;
timeout = setTimeout(function() {
func.apply(context, args)
clearTimeout(timeout)
timeout = null
}, time||500)
}
}
複製代碼
咱們實現了一個簡單的節流函數,可是還不夠完整,若是我想在第一次觸發的時候當即執行怎麼辦?若是我想禁用掉最後一次執行怎麼辦?underscore 中實現了一個比較完整的節流函數。segmentfault
// options是一個對象,若是options.leading爲false,就是禁用第一次觸發當即調用
// 若是options.trailing爲false,則是禁用第一次執行
_.throttle = function (func, wait, options) {
// 一些初始化操做
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function () {
// 若是禁用第一次首先執行,返回0不然就用previous保存當前時間戳
previous = options.leading === false ? 0 : _.now();
// 解除引用
timeout = null;
result = func.apply(context, args);
// 看到一種說法是在func函數裏面從新給timeout賦值,會致使timeout依然存在,因此這裏會判斷!timeout
if (!timeout) context = args = null;
};
return function () {
// 獲取當前調用時的時間(ms)
var now = _.now();
// 若是previous爲0而且禁用了第一次執行,那麼將previous設置爲當前時間
// 這裏用全等來避免undefined的狀況
if (!previous && options.leading === false) previous = now;
// 還要wait時間纔會觸發下一次func
var remaining = wait - (now - previous);
context = this;
args = arguments;
// remaining小於0有兩種狀況,一種是上次調用後到如今已經到了wait時間
// 一種狀況是第一次觸發的時候而且options.leading不爲false,previous爲0,由於now記錄的是unix時間戳,因此會遠遠大於wait
// remaining大於wait的狀況我本身不清楚,但看到一種說法是客戶端系統時間被調整過,可能會出現now小於previous的狀況
// 這兩種情形下會當即執行func函數,並把previous設置爲now
if (remaining <= 0 || remaining > wait) {
if (timeout) {
// 清除定時器
clearTimeout(timeout);
timeout = null;
}
// previous保存當前觸發的時間戳
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
// 若是timeout不存在(當前定時器還存在)
// 而且options.trailing不爲false,這個時候會從新設置定時器,remaining時間後執行later函數
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
複製代碼
這段代碼看着很少,可是讓我糾結了好久,運行的時候主要會有如下幾種狀況。bash
now - previous
幾乎爲0,這個時候知足 else if
裏面的判斷,會設置一個定時器,這個定時器在 remaining 時間後執行,因此只要在 remaining 時間內無論咱們再怎麼頻繁觸發,因爲不會知足兩個 if 裏面的條件,因此都不會執行 func,一直到 remaining 後纔會執行func這種狀況和上面狀況相似,不過區別在於第一次觸發的時候。微信
因爲知足 !previous && options.leading === false
這個條件,因此 previous 會被設置爲 now,這個時候 remaining 等於 wait,因此會走 else if
的分支,這樣就會重複前一種狀況下步驟2的流程app
remaining <= wait
,可是又由於 options.trailing
爲 false,這樣就不會走 if 的任何一個分支,一直到 now-previous
大於 wait 的時候(也就是過了 wait 時間後),這樣會知足 if 第一個分支的條件,func 會當即被執行一次最好不要這麼寫,由於會致使一個 bug 的出現,若是咱們在一段時間內頻繁觸發,這個是沒什麼問題,但若是咱們最後一次觸發後中止等待 wait 時間後再從新開始觸發,這時候的第一次觸發就會當即執行 func,leading 爲 false 並無生效。函數
不知道有沒有人和我同樣有這兩個疑問,leading 爲 false 的時候,真的只是在第一次調用的時候有區別嗎?trailing 是怎麼作到禁用最後一次執行的?源碼分析
這兩個問題讓我昨晚睡覺前都還在糾結,還好今天在 segmentfault 上面有熱心的用戶幫我解答了。
請直接看第一個回答以及下面的評論區:關於 underscore 源碼 中throttle 函數的疑惑?
GDUTxxZ 大神給了一段代碼,執行後不一樣的表現讓我印象深入。
var _now = new Date().getTime()
var throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
console.log(`函數${++i}在${new Date().getTime() - _now}調用`)
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 若是超過了wait時間,那麼就當即執行
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
var i = 0
var test = throttle(() => {
console.log(`函數${i}在${new Date().getTime() - _now}執行`)
}, 1000, {leading: false})
setInterval(test, 3000)
複製代碼
我將傳入 leading 和沒傳入 leading 的狀況做了如下比較。 leading 爲 false 時:
沒有傳入 leading 時: 當兩次觸發間隔時間大於 wait 時間的時候,很明顯 leading 爲 false 的時候總會在調用後延遲 wait 後執行 func ,而不傳 leading 的時候二者是同時的,調用 test 的時候就直接運行了 func。本來應該是callback => wait => callback
通常狀況下固然不會有這種極端狀況存在,可是可能出現這種狀況。若是在 scroll 事件中,咱們滾動一段距離後中止了,等 wait ms 後再開始滾動,這個時候若是 leading 爲 false,依然會延遲 wait 時間後執行,而不是當即執行,這也是爲何同時設置 leading 和 trailing 爲 false 的時候會出現問題。
trailing 爲 false 時究竟是怎麼禁用了最後一次調用?這個也一直讓我很糾結。一樣的,我也寫了一段代碼,比較了一下兩次運行後的不一樣結果。
var _now = new Date().getTime()
var throttle = function(func, wait, options) {
var context, args, result;
var timeout = null;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : new Date().getTime();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
return function() {
console.log(`函數${++i}在${new Date().getTime() - _now}調用`)
var now = new Date().getTime();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
// 若是超過了wait時間,那麼就當即執行
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
};
var i = 0
var test = throttle(() => {
console.log(函數${i}在${new Date().getTime() - _now}執行)
}, 1000, {trailing: false})
window.addEventListener("scroll", test)
複製代碼
trailing 爲 false 時:
沒有設置 trailing 時:
這兩張圖很明顯的不一樣就是設置了 trailing 的時候,最後一次老是"執行",而未設置 trailing 最後一次老是"調用",少了一次執行。
咱們能夠假設在一種臨界的場景下,好比在倒數第二次執行 func 後的 (wait-1) 的時間內。
若是設置了 trailing,由於沒法走 setTimeout,因此只能等待 wait 時間後才能當即調用 func,因此在(wait-1)的時間內不管咱們觸發了多少次都不會執行 func 函數。
若是沒有設置 trailing,那麼確定會走 setTimeout,在這個期間觸發的第一次就會設置一個定時器,等到 wait 時間後自動執行 func 函數,到(wait-1)的這段時間內無論咱們觸發了多少次,反正第一次觸發的時候就已經設置了定時器,因此到最後必定會執行一次 func 函數。
好久之前就使用過 throttle 函數,本身也實現過簡單的,可是看到 underscore 源碼後才發現原來還會有這麼多使人充滿想象的場景,本身所學的這點知識真的是皮毛。
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙: