1. 防抖與節(jié)流出現(xiàn)的背景
在日常搬磚中我們都會發(fā)現(xiàn)JS有很多高頻率觸發(fā)的事件,比如scroll职辨、mouseover、keydown之類的戈二。舉個實際例子舒裤,有個輸入框,在我們輸入的同時觉吭,希望向后端請求獲得自動補全的功能腾供,比如輸入“防”就可以提示“防抖節(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賦上一個定時器進行用戶的回調(diào)函數(shù)
如何實現(xiàn)節(jié)流:
第一步: 在 閉包內(nèi)
||全局
|| vue對象
之類的地方上定義一個隨時都能訪問到的變量timeout
第二步: 在事件觸發(fā)的時候,通過timeout判斷定時器是否存在瘤礁,如果存在就啥都不干阳懂,如果不存在就加一個定時器
這太容易了!讓我們看看lodash是怎么寫的防抖
吧柜思,附上源碼(源碼略多)岩调,他里面還會import一些其他函數(shù),大致就是一些簡單的功能函數(shù)赡盘,比如now獲取當前時間号枕,toNumber轉(zhuǎn)換到數(shù)字,isObject判斷是不是對象(這個函數(shù)還是個錯的陨享,基于typeof寫并不能正確判斷數(shù)據(jù)類型葱淳,但是應該也沒人會把一個new String(xxx)塞進去當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é)流復用防抖代碼
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ù)定硝,默認不執(zhí)行
maxing = false, // 是否有最大等待時間皿桑,默認沒有
trailing = true; // 是否在最后執(zhí)行一次回調(diào)函數(shù),默認執(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)用,防抖開始運作
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需要存在)
都是應該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)管毙,對應節(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ù)的包裝和閉包的應用裹虫,讓防抖和節(jié)流可以復用且不互相影響
- 按時間間隔制造定時器,沒有定時器的重復定義和消除的過程融击,通過 時間差 和 定時器存在狀態(tài) 來判斷是否添加新的定時器任務
- 靈活的參數(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出去萄窜,可以進行進一步的操作
// 比如我們給onscroll="scroll加一個防抖"
let throttle_test = throttle(CB, 1000, option)
function scroll () {
let a = throttle_test()
// 這里a會得到something趾访,然后進行相關(guān)操作
... doSomething with somethingFromCB
}
function functionCB () {
return somethingFromCB
}
8.節(jié)流的代碼復用(利用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, // 默認為true
'maxWait': wait, // maxWait為用戶設(shè)置的最大的等待時間衣摩,從而把防抖變成節(jié)流
'trailing': trailing // 默認為true
});
}
- 兩種定時器創(chuàng)建方式(定時器到時炼鞠、maxing到時的強制觸發(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的我舍棄了什么(為了表達的清晰一些怔接,代碼沒有封裝復用)
- 因為不復用搪泳,防抖節(jié)流各司其職,不需要通過maxWait實現(xiàn)兩套邏輯
- 舍棄了option扼脐,換了個immediate岸军,代表回調(diào)是在一開始還是最后執(zhí)行,類似于leading與trailing的作用
- 舍棄了時間的判斷瓦侮,按照一開始的簡易思路艰赞,防抖粗暴的新建和清除定時器,節(jié)流則是通過定時器的有無肚吏,來判斷是否建立新的定時器方妖,思路更加直觀,但是從底層去思考罚攀,時間的判斷雖然不直觀党觅,但是性能上應該比建立定時器和清除定時器要好很多雌澄,畢竟人家是個完善的JS庫。杯瞻。镐牺。所以結(jié)論應該是我的防抖比lodash的要差,但是我的節(jié)流也沒有建立重復的定時器而且少了不必要的時間判斷性能還會好些魁莉?2墙А?旗唁!
- 沒有了maxWait畦浓,不能制造防抖和節(jié)流的結(jié)合體
- 沒有return result,其實逆皮。宅粥。。你也不見得用得到你自己回調(diào)函數(shù)的返回值电谣,一般都是進行一種行為秽梅,上傳個東西,打個點剿牺,拉個數(shù)據(jù)啥的
- 沒有暴露cancel和flush方法企垦,其實。晒来。钞诡。一般你也用不到這個功能,而且這倆函數(shù)很好寫湃崩,一個干掉定時器荧降,一個執(zhí)行回調(diào)(順手也可以干掉定時器),有需要加上即可~