上次介紹了前端性能優化之防抖-debounce,此次來聊聊它的兄弟-節流。javascript
再拿乘電梯的例子來講:坐過電梯的都知道,在電梯關門但未上升或降低的一小段時間內,若是有人從外面按開門按鈕,電梯是會再開門的。要是電梯空間沒有限制的話,那裏面的人就一直在等。。。後來電梯工程師收到了好多投訴,因而他們就改變了方案,設定每隔必定時間,好比30秒,電梯就會關門,下一節電梯會繼續等待30秒。前端
專業術語歸納就是:每隔必定時間,執行一次函數。java
最簡易版的代碼實現:性能優化
function throttle(fn, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
};
}
複製代碼
很好理解,返回一個匿名函數造成閉包,並維護了一個局部變量timer。只有在timer不爲null纔開啓定時器,而timer爲null的時機則是定時器執行完畢。閉包
除了定時器,還能夠用時間戳實現:app
function throttle(fn, delay) {
let last = 0;
return function() {
const context = this;
const args = arguments;
const now = +new Date();
const offset = now - last;
if (offset > delay) {
last = now;
fn.apply(context, args);
}
};
}
複製代碼
last表明上次執行fn的時刻,每次執行匿名函數都會計算當前時刻與last的間隔,是否比咱們設定的時間間隔大,若大於,則執行fn,並更新last的值。前端性能
比較上述兩種實現方式,實際上是有區別的: 定時器方式,第一次觸發並不會執行fn,但中止觸發以後,還會再次執行一次fn 時間戳方式,第一次觸發會執行fn,中止觸發後,不會再次執行一次fn函數
兩種方式是能夠互補的,能夠將其結合起來,即能第一次觸發會執行fn,又能在中止觸發後,再次執行一次fn:post
function throttle(fn, delay) {
let last = 0;
let timer = null;
return function() {
const context = this;
const args = arguments;
const now = +new Date();
const offset = now - last;
if (offset > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
}
else if (!timer) {
timer = setTimeout(() => {
last = +new Date();
timer = null;
fn.apply(context, args);
}, delay - offset);
}
};
}
複製代碼
匿名函數內有個if...else,第一個是判斷時間戳,第二個是判判定時器,對比下前面兩種實現方式。 首先是時間戳方式的簡易版:性能
if (offset > delay) {
last = now;
fn.apply(context, args);
}
複製代碼
混合版:
if (offset > delay) {
if (timer) { // 注意這裏
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
}
複製代碼
能夠發現,混合版比簡易版多了對timer不爲null的判斷,並清除了定時器、將timer置爲null。 再是定時器實現方式的簡易版:
if (!timer) {
timer = setTimeout(() => {
fn.apply(context, args);
timer = null;
}, delay);
}
複製代碼
混合版:
else if (!timer) {
timer = setTimeout(() => {
last = +new Date(); // 注意這裏
timer = null;
fn.apply(context, args);
}, delay - offset);
}
複製代碼
能夠看到,混合版比簡易版多了對last變量的重置,而last變量是時間戳實現方式中判斷的重要因素。這裏要注意下,由於是在定時器的回調中,因此last的重置值要從新獲取當前時間戳,而不能使用變量now。
經過以上對比,咱們能夠發現,混合版是綜合了兩種不一樣實現方式的做用,但除去開始和結束階段的不一樣,二者的共同做用是一致的--執行fn函數。因此,同一個時刻,執行fn函數的語句只能存在一個!在混合版的實現中,時間戳判斷裏,去除了定時器的影響,定時器判斷裏,去除了時間戳的影響。
對於當即執行和中止觸發後的再次執行,咱們能夠經過參數來控制,適應需求的變化。 假設規定{ immediate: false } 阻止當即執行,{ trailing: false } 阻止中止觸發後的再次觸發:
function throttle(fn, delay, options = {}) {
let timer = null;
let last = 0;
return function() {
const context = this;
const args = arguments;
const now = +new Date();
if (last === 0 && options.immediate === false) { // 這個條件語句是新增的
last = now;
}
const offset = now - last;
if (offset > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
}
else if (!timer && options.trailing !== false) { // options.trailing !== false 是新增的
timer = setTimeout(() => {
last = options.immediate === false ? 0 : +new Date();;
timer = null;
fn.apply(context, args);
}, delay - offset);
}
};
}
複製代碼
相對於混合版,除了新增了一個參數options,其它不一樣之處已在代碼中標明。 思考下,當即執行是時間戳方式實現的,那麼想要阻止當即執行的話,只要阻止第一次觸發時,offset > delay 條件的成立就好了!如何判斷是第一次觸發?last變量只有初始化時,值纔會是0,再加上咱們手動傳入的參數,阻止當即執行的條件就知足了:
if (last === 0 && options.immediate === false) {
last = now;
}
複製代碼
條件知足後,咱們重置last變量的初始值爲當前時間戳,那麼第一次 offset > delay 就不會成立了! 而後想阻止中止觸發後的再次執行,仔細一想,要是不須要這個功能的話,時間戳的實現不就能夠知足了?對!咱們只要變相地去除定時器就行了:
!timer && options.trailing !== false
複製代碼
若是咱們不手動傳入{ trailing: false } ,這個條件是永遠不會成立的,即定時器永遠不會開啓。
不過有個問題在於,immediate和trailing不能同時設置爲false,緣由在於,{ trailing: false } 的話,中止觸發後不會再次執行,而後關鍵的last變量也就不會被重置爲0,下一次再次觸發又會當即執行,這樣就有衝突了。