前端效能優化之防抖-debounce
這周接到一個需求-給輸入框做模糊匹配。這還不簡單,監聽input事件,取到輸入值去調介面不就行了? 然而後端小哥說不行,這個介面的資料量非常大,這種方式呼叫介面的頻率太高,而且使用者輸入時呼叫根本沒有必要,只要在使用者停止輸入的那一刻切調介面就行了。 唉?這個場景聽起來怎麼這麼像防抖呢?
那到底什麼是防抖呢? 大家一定見過那種左右兩邊中間放廣告位的網站,在網頁滾動時,廣告位要保持在螢幕中間,就要不斷地去計算位置,如果不做限制,在視覺上廣告位就像在“抖”。防止這種情況,就叫防抖了!
防抖的原理是什麼? 我一直覺得網上流傳的例子非常形象:當我們在乘電梯時,如果這時有人過來,我們會出於禮貌一直按著開門按鈕等待,等到這人進電梯了,剛準備關門時,發現又有人過來了!我們又要重複之前的操作,如果電梯空間無限大的話,我們就要一直等待了。。。當然人的耐心是有限的!所以我們規定了一個時間,比如10秒,如果10秒都沒人來的話,就關電梯門。
用專業術語概括就是:在一定時間間隔內函式被觸發多次,但只執行最後一次。
最簡易版的程式碼實現:
function debounce(fn, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
};
}
複製程式碼
fn是要進行防抖的函式,delay是設定的延時,debounce返回一個匿名函式,形成閉包,內部維護了一個私有變數timer。我們一直會觸發的是這個返回的匿名函式,定時器會返回一個Id值賦給timer,如果在delay時間間隔內,匿名函式再次被觸發,定時器都會被清除,然後重新開始計時。
當然簡易版肯定不能滿足日常的需求,比如可能需要第一次立即執行的,所以要稍做改動:
function debounce(fn, delay, immediate ) {
let timer = null;
return function() {
const context = this;
const args = arguments;
timer && clearTimeout(timer);
if(immediate) {
const doNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
doNow && fn.apply(context, args);
}
else {
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
}
};
}
複製程式碼
比起簡易版,多了個引數immediate來區分是否需要立即執行。其它與簡易版幾乎一致的邏輯,除了判斷立即執行的地方:
const doNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
doNow && fn.apply(context, args);
複製程式碼
doNow變數的值為!timer,只有!timer為true的情況下,才會執行fn函式。第一次執行時,timer的初始值為null,所以會立即執行fn。接下來非第一次執行的情況下,等待delay時間後才能再次觸發執行fn。 注意!與簡易版的區別,簡易版是一定時間多次內觸發,執行最後一次。而立即執行版是不會執行最後一次的,需要再次觸發。
防抖的函式可能是有返回值,我們也要做相容:
function debounce(fn, delay, immediate) {
let timer = null;
return function() {
const context = this;
const args = arguments;
let result = undefined;
timer && clearTimeout(timer);
if (immediate) {
const doNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
if (doNow) {
result = fn.apply(context, args);
}
}
else {
timer = setTimeout(() => {
fn.apply(context, args);
}, delay);
}
return result;
};
}
複製程式碼
但是這個實現方式有個缺點,因為除了第一次立即執行,其它情況都是在定時器中執行的,也就是非同步執行,返回值會是undefined。
考慮到非同步,我們也可以返回Promise:
function debounce(fn, delay, immediate) {
let timer = null;
return function() {
const context = this;
const args = arguments;
return new Promise((resolve, reject) => {
timer && clearTimeout(timer);
if (immediate) {
const doNow = !timer;
timer = setTimeout(() => {
timer = null;
}, delay);
doNow && resolve(fn.apply(context, args));
}
else {
timer = setTimeout(() => {
resolve(fn.apply(context, args));
}, delay);
}
});
};
}
複製程式碼
如此,只要fn被執行,那必定可以拿到返回值!這也是防抖的終極版了!