1. 防抖與節(jié)流出現(xiàn)的背景
在日常搬磚中我們都會發(fā)現(xiàn)JS有很多高頻率觸發(fā)的事件,比如scroll、mouseover、keydown之類的。舉個實際例子,有個輸入框,在我們輸入的同時,希望向后端請求獲得自動補(bǔ)全的功能,比如輸入“防”就可以提示“防抖節(jié)流”,但是我們又不希望每次keydown或者input的change都發(fā)起一個新的請求,這時候我們就需要使用到防抖和節(jié)流了
2. 防抖節(jié)流的概念
防抖:設(shè)置一個時間間隔K秒,在K秒內(nèi)多次觸發(fā)事件,只會在最后一次事件結(jié)束后K秒觸發(fā)事件回調(diào),如果在最后一次事件結(jié)束不滿K秒的過程中再次觸發(fā)時間則會清除掉之前的定時器,并重新計時。
節(jié)流:設(shè)置一個時間間隔K秒,開始頻繁的觸發(fā)事件,事件每隔K秒便會觸發(fā)一次
3. lodash防抖節(jié)流實現(xiàn)
不考慮別人的庫是怎么實現(xiàn)的,最直觀的講如何實現(xiàn)防抖:
第一步: 在 閉包內(nèi) ||全局 || vue對象 之類的地方上定義一個隨時都能訪問到的變量timeout
第二步: 在事件觸發(fā)的時候,通過timeout判斷定時器是否存在,存在就clear掉,不存在就給timeout賦上一個定時器進(jìn)行用戶的回調(diào)函數(shù)
如何實現(xiàn)節(jié)流:
第一步: 在 閉包內(nèi) ||全局 || vue對象 之類的地方上定義一個隨時都能訪問到的變量timeout
第二步: 在事件觸發(fā)的時候,通過timeout判斷定時器是否存在,如果存在就啥都不干,如果不存在就加一個定時器
這太容易了!讓我們看看lodash是怎么寫的防抖吧,附上源碼(源碼略多),他里面還會import一些其他函數(shù),大致就是一些簡單的功能函數(shù),比如now獲取當(dāng)前時間,toNumber轉(zhuǎn)換到數(shù)字,isObject判斷是不是對象(這個函數(shù)還是個錯的,基于typeof寫并不能正確判斷數(shù)據(jù)類型,但是應(yīng)該也沒人會把一個new String(xxx)塞進(jìn)去當(dāng)option)
function debounce(func, wait, options) {
var lastArgs, // arguments暫存(因為arguments和this都是debounce函數(shù)接收的)
lastThis, // this暫存 (而最終調(diào)用卻是invokeFunc函數(shù),所以需要利用閉包暫存變量)
maxWait, // option中maxWait最大等待時間字段,代表超過這個時間,回調(diào)可以再次被觸發(fā),用于節(jié)流復(fù)用防抖代碼
result, // return出去的回調(diào)函數(shù)的返回值,回調(diào)有返回值&&回調(diào)被觸發(fā)才有值
timerId, // 定時器對象
lastCallTime, // 上次調(diào)用debounced的時間
lastInvokeTime = 0, // 最后一次調(diào)用用戶回調(diào)的時間
leading = false, // 是否立即執(zhí)行一次回調(diào)函數(shù),默認(rèn)不執(zhí)行
maxing = false, // 是否有最大等待時間,默認(rèn)沒有
trailing = true; // 是否在最后執(zhí)行一次回調(diào)函數(shù),默認(rèn)執(zhí)行
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
// 綁定暫存的arguments和this,執(zhí)行用戶回調(diào),并返回函數(shù)的返回值
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
// isInvoking為true && 沒有建立定時器時調(diào)用,防抖開始運(yùn)作
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
// 計算需等待時間
function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
timeWaiting = wait - timeSinceLastCall;
return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
// 判斷是否能invoke,用于來判斷是否能 制造定時器 來執(zhí)行用戶的回調(diào)函數(shù)
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
/**
lastCallTime未定義||
這次調(diào)用和上次調(diào)用的差不小于用戶傳入的間隔||
該間隔小于0||
設(shè)置的最大間隔時間存在,差值超過了最大間隔時間(為節(jié)流設(shè)計,maxing需要存在)
都是應(yīng)該Invoke的狀態(tài)
**/
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}
// 等待時間結(jié)束后,如果能invoke,通過trailingEdge觸發(fā)用戶回調(diào),如果不能 計算剩余時間 再重置定時器
function timerExpired() {
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
timerId = setTimeout(timerExpired, remainingWait(time));
}
// 等待時間結(jié)束后的執(zhí)行邏輯 :
// 清空定時器變量,如果 最后需要執(zhí)行回調(diào) && 暫存的arguments存在 執(zhí)行用戶回調(diào)
function trailingEdge(time) {
timerId = undefined;
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
// 暴露的接口,手動取消防抖
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
// 暴露的接口,如果定時器存在,直接觸發(fā)回調(diào)
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
// 防抖核心函數(shù)
function debounced() {
var time = now(), isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
if (isInvoking) { // 一開始lastCallTime為undefined所以isInvoking為true
if (timerId === undefined) {
return leadingEdge(lastCallTime); // 這個是初始狀態(tài)
}
if (maxing) { // 節(jié)流觸發(fā)方式之一:時間到了,重造定時器,并執(zhí)行回調(diào)
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
/**
這段邏輯和正常(只觸發(fā)一次)的防抖無關(guān),對應(yīng)節(jié)流觸發(fā)方式之二:trailingEdge(定時器正常到時間)
正常的trailingEdge不會新建新的定時器,所以需要在這里新建定時器
兩種觸發(fā)方式互斥,通過觸發(fā)后影響isInvoking的狀態(tài)防止二次觸發(fā)
**/
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
這一堆代碼相比之前極簡的防抖節(jié)流有什么區(qū)別都提供了什么功能?
-
lodash防抖實現(xiàn)邏輯: 通過不斷更新now和lastCallTime,讓shouldInvoke始終返回false無法觸發(fā)回調(diào),直到事件不觸發(fā)不更新lastCallTime才能觸發(fā)回調(diào) -
lodash節(jié)流實現(xiàn)邏輯:通過option中maxWait的傳入,放寬了shouldInvoke的判定條件,使定時器能夠正常觸發(fā)用戶定義的回調(diào)函數(shù),并在事件的循環(huán)回調(diào)中加入maxing判斷邏輯,作為觸發(fā)用戶定義回調(diào)的第二入口,并通過影響isInvoking的狀態(tài)防止二次觸發(fā) - 函數(shù)的包裝和閉包的應(yīng)用,讓防抖和節(jié)流可以復(fù)用且不互相影響
- 按時間間隔制造定時器,沒有定時器的重復(fù)定義和消除的過程,通過 時間差 和 定時器存在狀態(tài) 來判斷是否添加新的定時器任務(wù)
- 靈活的參數(shù),通過leading和trailing來控制回調(diào)在一連串的事件行為的開始還是結(jié)束時被觸發(fā)
- lastArgs、lastThis讓函數(shù)之間通信更加便利
// 先定義一個防抖
let throttle_a = throttle(functionCB(){...}, waitTime),
// 然后再對其傳參
throttle_a(param1,...,paramN)
// 這些參數(shù)可以在functionCB中訪問
function functionCB () {
console.log(arguments) // 可以拿到上面的param1,...,paramN
}
- 平時為undefined的result會在回調(diào)執(zhí)行后,賦上functionCB的返回值,并被return出去,可以進(jìn)行進(jìn)一步的操作
// 比如我們給onscroll="scroll加一個防抖"
let throttle_test = throttle(CB, 1000, option)
function scroll () {
let a = throttle_test()
// 這里a會得到something,然后進(jìn)行相關(guān)操作
... doSomething with somethingFromCB
}
function functionCB () {
return somethingFromCB
}
8.節(jié)流的代碼復(fù)用(利用option中的maxWait打通debounced中if (maxing) {...}中的邏輯)
function throttle(func, wait, options) {
var leading = true, trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
'leading': leading, // 默認(rèn)為true
'maxWait': wait, // maxWait為用戶設(shè)置的最大的等待時間,從而把防抖變成節(jié)流
'trailing': trailing // 默認(rèn)為true
});
}
- 兩種定時器創(chuàng)建方式(定時器到時、maxing到時的強(qiáng)制觸發(fā)),可以構(gòu)建出一種防抖和節(jié)流的結(jié)合體,比如wait定為1000,maxWait定為5000,也就是在5000ms內(nèi)連續(xù)折騰只能觸發(fā)一次,但是5000ms后又可以觸發(fā),使用更加靈活
4. 自己造一套簡易版的防抖節(jié)流
防抖:
function debounce (func, wait, immediate = true) {
let timeout, _this, args;
let later = () => setTimeout (() => {
timeout = null
if (!immediate) {
func.apply(_this, args);
_this = args = null;
}
},wait);
let debounced = function (...params) {
if (!timeout) {
timeout = later();
if (immediate){
func.apply(this, params);
} else {
_this = this;
args = params;
}
} else {
clearTimeout(timeout);
timeout = later();
}
}
return debounced;
};
節(jié)流:
function throttle (func, wait, immediate = true) {
let timeout, _this, args
let later = () => setTimeout (() => {
timeout = null
if (!immediate) {
func.apply(_this, args);
_this = args = null;
}
}, wait);
let throttled = function (...params) {
if (!timeout) {
timeout = later();
if (immediate){
immediate = false
func.apply(this, params);
}
_this = this;
args = params;
}
}
return throttled;
};
相比lodash的我舍棄了什么(為了表達(dá)的清晰一些,代碼沒有封裝復(fù)用)
- 因為不復(fù)用,防抖節(jié)流各司其職,不需要通過maxWait實現(xiàn)兩套邏輯
- 舍棄了option,換了個immediate,代表回調(diào)是在一開始還是最后執(zhí)行,類似于leading與trailing的作用
- 舍棄了時間的判斷,按照一開始的簡易思路,防抖粗暴的新建和清除定時器,節(jié)流則是通過定時器的有無,來判斷是否建立新的定時器,思路更加直觀,但是從底層去思考,時間的判斷雖然不直觀,但是性能上應(yīng)該比建立定時器和清除定時器要好很多,畢竟人家是個完善的JS庫。。。所以結(jié)論應(yīng)該是我的防抖比lodash的要差,但是我的節(jié)流也沒有建立重復(fù)的定時器而且少了不必要的時間判斷性能還會好些????!
- 沒有了maxWait,不能制造防抖和節(jié)流的結(jié)合體
- 沒有return result,其實。。。你也不見得用得到你自己回調(diào)函數(shù)的返回值,一般都是進(jìn)行一種行為,上傳個東西,打個點(diǎn),拉個數(shù)據(jù)啥的
- 沒有暴露cancel和flush方法,其實。。。一般你也用不到這個功能,而且這倆函數(shù)很好寫,一個干掉定時器,一個執(zhí)行回調(diào)(順手也可以干掉定時器),有需要加上即可~