作為一名前端開發(fā)者,我們經(jīng)常會處理各種事件蔑赘,比如常見的click狸驳、scroll、 resize等等缩赛。仔細(xì)一想耙箍,會發(fā)現(xiàn)像scroll、scroll酥馍、onchange這類事件會頻繁觸發(fā)辩昆,如果我們在回調(diào)中計(jì)算元素位置、做一些跟DOM相關(guān)的操作旨袒,引起瀏覽器回流和重繪汁针,頻繁觸發(fā)回調(diào),很可能會造成瀏覽器掉幀砚尽,甚至?xí)篂g覽器崩潰施无,影響用戶體驗(yàn)。針對這種現(xiàn)象必孤,目前有兩種常用的解決方案:防抖和節(jié)流帆精。
防抖(debounce)
所謂防抖,就是指觸發(fā)事件后隧魄,就是把觸發(fā)非常頻繁的事件合并成一次去執(zhí)行卓练。即在指定時(shí)間內(nèi)只執(zhí)行一次回調(diào)函數(shù),如果在指定的時(shí)間內(nèi)又觸發(fā)了該事件购啄,則回調(diào)函數(shù)的執(zhí)行時(shí)間會基于此刻重新開始計(jì)算襟企。
以我們生活中乘車刷卡的情景舉例,只要乘客不斷地在刷卡狮含,司機(jī)師傅就不能開車顽悼,乘客刷卡完畢之后曼振,司機(jī)會等待幾分鐘,確定乘客坐穩(wěn)再開車蔚龙。如果司機(jī)在最后等待的時(shí)間內(nèi)又有新的乘客上車冰评,那么司機(jī)等乘客刷卡完畢之后,還要再等待一會木羹,等待所有乘客坐穩(wěn)再開車甲雅。
image.png
具體應(yīng)該怎么去實(shí)現(xiàn)這樣的功能呢嘱丢?第一時(shí)間肯定會想到使用setTimeout方法侣诺,那我們就嘗試寫一個(gè)簡單的函數(shù)來實(shí)現(xiàn)這個(gè)功能吧~
var debounce = function(fn, delayTime) {
var timeId;
return function() {
var context = this, args = arguments;
timeId && clearTimeout(timeId);
timeId = setTimeout(function(){
fn.apply(context, args);
}, delayTime)
}
}
思路解析:
執(zhí)行debounce函數(shù)之后會返回一個(gè)新的函數(shù),通過閉包的形式糜芳,維護(hù)一個(gè)變量timeId脐瑰,每次執(zhí)行該函數(shù)的時(shí)候會結(jié)束之前的延遲操作妖枚,重新執(zhí)行setTimeout方法,也就實(shí)現(xiàn)了上面所說的指定的時(shí)間內(nèi)多次觸發(fā)同一個(gè)事件苍在,會合并執(zhí)行一次绝页。
溫馨提示:
1、上述代碼中arguments只會保存事件回調(diào)函數(shù)中的參數(shù)寂恬,譬如:事件對象等抒寂,并不會保存fn、delayTime
2掠剑、使用apply改變傳入的fn方法中的this指向,指向綁定事件的DOM元素郊愧。
節(jié)流(throttle)
所謂節(jié)流朴译,是指頻繁觸發(fā)事件時(shí),只會在指定的時(shí)間段內(nèi)執(zhí)行事件回調(diào)属铁,即觸發(fā)事件間隔大于等于指定的時(shí)間才會執(zhí)行回調(diào)函數(shù)眠寿。
類比到生活中的水龍頭,擰緊水龍頭到某種程度會發(fā)現(xiàn)焦蘑,每隔一段時(shí)間盯拱,就會有水滴流出。
說到時(shí)間間隔例嘱,大家肯定會想到使用setTimeout來實(shí)現(xiàn)狡逢,在這里,我們使用兩種方法來簡單實(shí)現(xiàn)這種功能:時(shí)間戳和setTimeout定時(shí)器拼卵。
時(shí)間戳
var throttle = (fn, delayTime) => {
var _start = Date.now();
return function() {
var _now = Date.now(), context = this, args = arguments;
if(_now - _start >= delayTime) {
fn.apply(context, args);
_start = Date.now();
}
}
}
通過比較兩次時(shí)間戳的間隔是否大于等于我們事先指定的時(shí)間來決定是否執(zhí)行事件回調(diào)奢浑。
定時(shí)器
var throttle = function(fn, delayTime) {
var flag;
return function() {
var context = this, args = arguments;
if(!flag) {
flag = setTimeout(function() {
fn.apply(context, args);
flag = false;
}, delayTime);
}
}
}
在上述實(shí)現(xiàn)過程中,我們設(shè)置了一個(gè)標(biāo)志變量flag腋腮,當(dāng)delayTime之后執(zhí)行事件回調(diào)雀彼,便會把這個(gè)變量重置壤蚜,表示一次回調(diào)已經(jīng)執(zhí)行結(jié)束。 對比上述兩種實(shí)現(xiàn)徊哑,我們會發(fā)現(xiàn)一個(gè)有趣的現(xiàn)象:
1袜刷、使用時(shí)間戳方式,頁面加載的時(shí)候就會開始計(jì)時(shí)莺丑,如果頁面加載時(shí)間大于我們設(shè)定的delayTime著蟹,第一次觸發(fā)事件回調(diào)的時(shí)候便會立即fn,并不會延遲窒盐。如果最后一次觸發(fā)回調(diào)與前一次觸發(fā)回調(diào)的時(shí)間差小于delayTime草则,則最后一次觸發(fā)事件并不會執(zhí)行fn;
2蟹漓、使用定時(shí)器方式炕横,我們第一次觸發(fā)回調(diào)的時(shí)候才會開始計(jì)時(shí),如果最后一次觸發(fā)回調(diào)事件與前一次時(shí)間間隔小于delayTime葡粒,delayTime之后仍會執(zhí)行fn份殿。
這兩種方式有點(diǎn)優(yōu)勢互補(bǔ)的意思,哈哈~
我們考慮把這兩種方式結(jié)合起來嗽交,便會在第一次觸發(fā)事件時(shí)執(zhí)行fn卿嘲,最后一次與前一次間隔比較短,delayTime之后再次執(zhí)行fn夫壁。
想法簡單實(shí)現(xiàn)如下:
var throttle = function(fn, delayTime) {
var flag, _start = Date.now();
return function() {
var context = this,
args = arguments,
_now = Date.now(),
remainTime = delayTime - (_now - _start);
if(remainTime <= 0) {
fn.apply(this, args);
} else {
setTimeout(function () {
fn.apply(this, args);
}, remainTime)
}
}
通過上面的分析拾枣,可以很明顯的看出函數(shù)防抖和函數(shù)節(jié)流的區(qū)別:
頻繁觸發(fā)事件時(shí),函數(shù)防抖只會在最后一次觸發(fā)事件只會才會執(zhí)行回調(diào)內(nèi)容盒让,其他情況下會重新計(jì)算延遲事件梅肤,而函數(shù)節(jié)流便會很有規(guī)律的每隔一定時(shí)間執(zhí)行一次回調(diào)函數(shù)。
requestAnimationFrame
之前邑茄,我們使用setTimeout簡單實(shí)現(xiàn)了防抖和節(jié)流功能姨蝴,如果我們不考慮兼容性,追求精度比較高的頁面效果肺缕,可以考慮試試html5提供的API--requestAnimationFrame左医。
與setTimeout相比,requestAnimationFrame的時(shí)間間隔是由系統(tǒng)來決定同木,保證屏幕刷新一次浮梢,回調(diào)函數(shù)只會執(zhí)行一次,比如屏幕的刷新頻率是60HZ彤路,即間隔1000ms/60會執(zhí)行一次回調(diào)黔寇。
var throttle = function(fn, delayTime) {
var flag;
return function() {
if(!flag) {
requestAnimationFrame(function() {
fn();
flag = false;
});
flag = true;
}
}
}
上述代碼的基本功能就是保證在屏幕刷新的時(shí)候(對于大多數(shù)的屏幕來說,大約16.67ms)斩萌,可以執(zhí)行一次回調(diào)函數(shù)fn缝裤。使用這種方式也存在一種比較明顯的缺點(diǎn)屏轰,時(shí)間間隔只能跟隨系統(tǒng)變化,我們無法修改憋飞,但是準(zhǔn)確性會比setTimeout高一些霎苗。
注意:
- 防抖和節(jié)流只是減少了事件回調(diào)函數(shù)的執(zhí)行次數(shù),并不會減少事件的觸發(fā)頻率榛做。
- 防抖和節(jié)流并沒有從本質(zhì)上解決性能問題唁盏,我們還應(yīng)該注意優(yōu)化我們事件回調(diào)函數(shù)的邏輯功能,避免在回調(diào)中執(zhí)行比較復(fù)雜的DOM操作检眯,減少瀏覽器reflow和repaint厘擂。
上面的示例代碼比較簡單,只是說明了基本的思路锰瘸。目前已經(jīng)有工具庫實(shí)現(xiàn)了這些功能刽严,比如underscore,考慮的情況也會比較多避凝,大家可以去查看源碼舞萄,學(xué)習(xí)作者的思路,加深理解管削。
underscore的debounce方法源碼:
_.debounce = function(func, wait, immediate) {
var timeout, result;
var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
underscore的throttle源碼:
_.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
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;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};