throttle(func, wait, options)
節(jié)流函數(shù),返回一個函數(shù)的節(jié)流版本;所謂節(jié)流版本,就是給需要執(zhí)行的函數(shù)一個執(zhí)行間隔:每隔waitms才執(zhí)行一次func。
寫個很簡單的節(jié)流函數(shù)還是很簡單的
var throttle = function (func, wait) {
var context, args;
var flag = true;
var later = function () {
flag = true;
}
return function () {
var result;
var context = this, args = arguments;
if (flag) {
result = func.apply(context, arguments);
flag = false;
setTimeout(later, wait);
}
return result;
}
}
我們用一個flag變量來控制目標函數(shù)的執(zhí)行,通過定時器來改變flag的值;看上去節(jié)流函數(shù)應該是實現(xiàn)了。但是我們發(fā)現(xiàn),這個節(jié)流函數(shù)默認不會執(zhí)行最后一次執(zhí)行,除非時間卡得好;而且會默認執(zhí)行第一次函數(shù)執(zhí)行,當然這可以通過改變flag初始值來解決。
underscore里對節(jié)流函數(shù)還有個要求,就是options參數(shù):如果你想禁用第一次首先執(zhí)行的話,傳遞{leading: false},還有如果你想禁用最后一次執(zhí)行的話,傳遞{trailing: false}。
我們可以通過判斷options.leading來決定flag的初始值;那么如果執(zhí)行最后一次執(zhí)行(這里的最后一次執(zhí)行當然是指函數(shù)執(zhí)行時間發(fā)生在wait期間),該怎么做呢?
通過閱讀underscore源碼,我發(fā)現(xiàn)作者思路是這樣的:被節(jié)流的函數(shù)雖然不會執(zhí)行,卻會生成一個唯一的定時器;這個定時器只會被正確執(zhí)行的函數(shù)銷毀,如果沒有被銷毀,定時器就會執(zhí)行所謂的“最后一次執(zhí)行”。同時后續(xù)的每一二個沒有被正確執(zhí)行的函數(shù),都會更新這個定時器要執(zhí)行的函數(shù)的this和args
那么思路清晰了,我們來寫這樣一個版本的節(jié)流函數(shù)
var throttle = function (func, wait, options) {
// 返回函數(shù)的this指針,參數(shù),以及返回結(jié)果
var result, context, args;
// 維護定時器
var timeout = null;
// 維護上一次函數(shù)執(zhí)行的時間
var previous = 0;
options = options || {};
var later = function () {
// 這個函數(shù)在某一連續(xù)執(zhí)行階段的最后執(zhí)行的
// 因此previous需要像一開始那樣初始化
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
}
return function () {
var now = _.now();
// 這里是關(guān)鍵,決定了函數(shù)的執(zhí)行與最后一次相關(guān)
context = this, args = arguments;
if (!previous && options.leading === false) {
previous = now;
}
// 計算下一次觸發(fā)的時間
var remaining = wait - (now - previous);
// 已經(jīng)到了觸發(fā)的時間 || 人為修改了時間
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
//記錄這一次代碼執(zhí)行的時間
previous = now;
// 執(zhí)行代碼
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
// 從這里看, 定時器似乎是寫在第二次函數(shù)觸發(fā)的,似乎并不跟最后一次函數(shù)執(zhí)行對應
// 但實際上,雖然定時器是在第二次函數(shù)觸發(fā),但是其參數(shù)會在最后一次函數(shù)執(zhí)行時重新賦值。
timeout = setTimeout(later, remaining);
}
return result;
}
}
debounce(func, wait, immediate)
防抖函數(shù)的場景差不多都類似于這兩種:
- 當用戶輸入,導致搜索框的內(nèi)容發(fā)生變化時,我們希望函數(shù)的觸發(fā)是在內(nèi)容發(fā)生變化
waitms并且沒有變化后才發(fā)出請求 - 用戶(更多是測試……)連續(xù)點擊一個
button,我們希望只有首次點擊觸發(fā)事件;后waitms內(nèi)的幾次觸發(fā)都不再觸發(fā)事件。
這樣分析,我們發(fā)現(xiàn)防抖函數(shù)的要求有兩種,一種是在waitms的開始觸發(fā),一種是在waitms之后觸發(fā)。這就是debounce中immediate設計的意義。
var debounce = function (func, wait, immediate) {
var context, result, args;
// 計時器
var timeout = null;
// 記錄上次代碼執(zhí)行時間戳
var timestamp;
var later = function () {
// 計算自上次執(zhí)行代碼過了多久
var last = _.now() - timestamp;
if (last < wait && last >= 0) {
// 如果此時處于代碼執(zhí)行后wait ms內(nèi)
// 重新設置計時器
timeout = setTimeout(later, wait - last);
} else {
// 此時代碼已經(jīng)執(zhí)行了wait ms
timeout = null;
if (!immediate) {
result = func.apply(context, args);
if (!timeout) context = args = null;
}
}
}
return function () {
// 記錄執(zhí)行狀態(tài)
context = arguments, context = this;
// 記錄執(zhí)行時間戳
timestamp = _.now();
// 代碼是否立即執(zhí)行
var canNow = immediate && !timeout;
if (canNow) {
result = func.apply(context, args);
context = args = null;
}
// 設置定時器
if (!timeout) {
time = setTimeout(later, wait);
}
}
}
如果immediate為true,canNow變量就會生效,第一次執(zhí)行就會執(zhí)行代碼;并且在之后的計時器里,只要計時器存在,代碼就不會被執(zhí)行。
如果immediate為false,每次執(zhí)行防抖函數(shù)都會更新時間戳;定時器自設置waitms后就會比較當前時間和時間戳,如果相差waitms,才會執(zhí)行代碼。
想到的額外的拓展
比如說觸底刷新,