上次介紹了前端性能優(yōu)化之防抖-debounce,這次來聊聊它的兄弟-節(jié)流。
再拿乘電梯的例子來說:坐過電梯的都知道,在電梯關(guān)門但未上升或下降的一小段時間內(nèi),如果有人從外面按開門按鈕,電梯是會再開門的。要是電梯空間沒有限制的話,那里面的人就一直在等。。。后來電梯工程師收到了好多投訴,于是他們就改變了方案,設(shè)定每隔一定時間,比如30秒,電梯就會關(guān)門,下一節(jié)電梯會繼續(xù)等待30秒。
專業(yè)術(shù)語概括就是:每隔一定時間,執(zhí)行一次函數(shù)。
最簡易版的代碼實現(xiàn):
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);
}
};
}
很好理解,返回一個匿名函數(shù)形成閉包,并維護(hù)了一個局部變量timer。只有在timer不為null才開啟定時器,而timer為null的時機(jī)則是定時器執(zhí)行完畢。
除了定時器,還可以用時間戳實現(xiàn):
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代表上次執(zhí)行fn的時刻,每次執(zhí)行匿名函數(shù)都會計算當(dāng)前時刻與last的間隔,是否比我們設(shè)定的時間間隔大,若大于,則執(zhí)行fn,并更新last的值。
比較上述兩種實現(xiàn)方式,其實是有區(qū)別的:
定時器方式,第一次觸發(fā)并不會執(zhí)行fn,但停止觸發(fā)之后,還會再次執(zhí)行一次fn
時間戳方式,第一次觸發(fā)會執(zhí)行fn,停止觸發(fā)后,不會再次執(zhí)行一次fn
兩種方式是可以互補(bǔ)的,可以將其結(jié)合起來,即能第一次觸發(fā)會執(zhí)行fn,又能在停止觸發(fā)后,再次執(zhí)行一次fn:
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);
}
};
}
匿名函數(shù)內(nèi)有個if...else,第一個是判斷時間戳,第二個是判斷定時器,對比下前面兩種實現(xiàn)方式。
首先是時間戳方式的簡易版:
if (offset > delay) {
last = now;
fn.apply(context, args);
}
混合版:
if (offset > delay) {
if (timer) { // 注意這里
clearTimeout(timer);
timer = null;
}
last = now;
fn.apply(context, args);
}
可以發(fā)現(xiàn),混合版比簡易版多了對timer不為null的判斷,并清除了定時器、將timer置為null。
再是定時器實現(xiàn)方式的簡易版:
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變量是時間戳實現(xiàn)方式中判斷的重要因素。這里要注意下,因為是在定時器的回調(diào)中,所以last的重置值要重新獲取當(dāng)前時間戳,而不能使用變量now。
通過以上對比,我們可以發(fā)現(xiàn),混合版是綜合了兩種不同實現(xiàn)方式的作用,但除去開始和結(jié)束階段的不同,兩者的共同作用是一致的--執(zhí)行fn函數(shù)。所以,同一個時刻,執(zhí)行fn函數(shù)的語句只能存在一個!在混合版的實現(xiàn)中,時間戳判斷里,去除了定時器的影響,定時器判斷里,去除了時間戳的影響。
對于立即執(zhí)行和停止觸發(fā)后的再次執(zhí)行,我們可以通過參數(shù)來控制,適應(yīng)需求的變化。
假設(shè)規(guī)定{ immediate: false } 阻止立即執(zhí)行,{ trailing: false } 阻止停止觸發(fā)后的再次觸發(fā):
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);
}
};
}
相對于混合版,除了新增了一個參數(shù)options,其它不同之處已在代碼中標(biāo)明。
思考下,立即執(zhí)行是時間戳方式實現(xiàn)的,那么想要阻止立即執(zhí)行的話,只要阻止第一次觸發(fā)時,offset > delay 條件的成立就行了!如何判斷是第一次觸發(fā)?last變量只有初始化時,值才會是0,再加上我們手動傳入的參數(shù),阻止立即執(zhí)行的條件就滿足了:
if (last === 0 && options.immediate === false) {
last = now;
}
條件滿足后,我們重置last變量的初始值為當(dāng)前時間戳,那么第一次 offset > delay 就不會成立了!
然后想阻止停止觸發(fā)后的再次執(zhí)行,仔細(xì)一想,要是不需要這個功能的話,時間戳的實現(xiàn)不就可以滿足了?對!我們只要變相地去除定時器就好了:
!timer && options.trailing !== false
如果我們不手動傳入{ trailing: false } ,這個條件是永遠(yuǎn)不會成立的,即定時器永遠(yuǎn)不會開啟。
不過有個問題在于,immediate和trailing不能同時設(shè)置為false,原因在于,{ trailing: false } 的話,停止觸發(fā)后不會再次執(zhí)行,然后關(guān)鍵的last變量也就不會被重置為0,下一次再次觸發(fā)又會立即執(zhí)行,這樣就有沖突了。