debounce
和 throttle
,用中文描述的話,就是 去抖
和 節流
。javascript
它們有什麼用:css
針對一些
執行頻率很是高
的交互或事件,作性能優化。java
兩者的概念,網上的說法不少,這裏不描述了。我這裏主要分析下他們的相同點和不一樣點,和在何時用它們。segmentfault
debounce
和 throttle
的相同點:api
都是利用函數延遲執行來實現效果,咱們暫時能夠理解成用
setTimeout
瀏覽器
注:lodash 中爲了優化性能,在沒有傳入 wait 參數的狀況下,優先使用 requestAnimationFrame,若是瀏覽器不支持,再降級使用 setTimeout
性能優化
debounce
和 throttle
的不一樣點:閉包
debounce
有一個等待時長,若是在這個等待時長內,再次調用了函數,就取消上一個定時器,並新建一個定時器。因此debounce
適用於input
,keyup
,keydown
等事件, 亦或者click
事件須要防止用戶在某個時間範圍內屢次點擊的時候,也能夠用。注:在lodash
的實現中 中並無取消新建
定時器的作法,是用時間來判斷的。app
throttle
也有一個等待時長,每隔一段這個等待時長,函數必須執行一次。若是在這個等待時長內,當前延遲執行沒有完成,它會忽略接下來調用該函數的請求,不會去取消上一個定時器。因此throttle
適用於scroll
,mousemove
等事件。在lodash
的實現中,還有一個等待的最大時長,這個咱們分析源碼時再討論。函數
resize
事件,使用debounce
或throttle
都行,看你的需求啦。
舉個栗子,使用 lodash
處理 resize
事件時,在 wait
參數不是很是小的狀況下:
用debounce
的話,會在用戶中止改變
瀏覽器窗口大小時觸發,也就是隻是在最後觸發一次。
用throttle
的話,會在用戶改變瀏覽器窗口大小的過程當中,每隔一段時間觸發一次。
throttle
就是一個定義了最大等待時長的 debounce
。接下來,咱們先本身實現 簡易版的 debounce
和 throttle
,而後再分析lodash
源碼中的對應方法
debounce
有個很重要的特性,就是在規定的等待時長內若是再次調用函數,會取消
上一次函數執行。
因此咱們這裏能夠用 clearTimeout
先清除定時器,再從新 setTimeout
建一個新的定時器。
它的原理其實就是在閉包內維護一個 定時器
。
function debounce(fn, wait) {
let callback = fn;
let timerId = null;
function debounced() {
// 保存做用域
let context = this;
// 保存參數,例如 event 對象
let args = arguments;
clearTimeout(timerId);
timerId = setTimeout(function() {
callback.apply(context, args);
}, wait);
}
// 返回一個閉包
return debounced;
}
// test
let resizeFun = function(e) {
console.log('resize');
};
window.addEventListener('resize', debounce(resizeFun, 500));
複製代碼
throttle
相對於 debounce
的最大區別就是它不會取消
上一次函數的執行。
因此咱們能夠基於 debounce
去調整一下。
function throttle(fn, wait) {
let callback = fn;
let timerId = null;
// 是不是第一次執行
let firstInvoke = true;
function throttled() {
let context = this;
let args = arguments;
// 若是是第一次觸發,直接執行
if (firstInvoke) {
callback.apply(context, args);
firstInvoke = false;
return ;
}
// 若是定時器已存在,直接返回。
if (timerId) {
return ;
}
timerId = setTimeout(function() {
// 注意這裏 將 clearTimeout 放到 內部來執行了
clearTimeout(timerId);
timerId = null;
callback.apply(context, args);
}, wait);
}
// 返回一個閉包
return throttled;
}
// test
let resizeFun = function(e) {
console.log('resize');
};
window.addEventListener('resize', throttle(resizeFun, 500));
複製代碼
function debounce(func, wait, options) {
let lastArgs, // 存儲 func 函數執行時的參數, 執行 debounced 函數的時候,被賦值
lastThis, // 存儲 func 函數執行時的做用域, 執行 debounced 函數的時候,被賦值
maxWait, // 最長等待時間
result, // 存儲 func 函數的返回值
timerId, // 定時器 id
lastCallTime // 最近一次 執行 debounced 函數時的時間
// 最近一次執行 func 時的時間戳
// 正常狀況下,lastCallTime 與 lastInvokeTime 是相差無幾的。
let lastInvokeTime = 0
// 是否 在延遲開始前 調用函數 - 它的做用,相似我上面實現的 throttle 方法中的 firstInvoke
let leading = false
// options 是否 傳入了 maxWait
let maxing = false
// 是否 在延遲結束後 調用函數
let trailing = true
// 能夠看到, debounce 函數,默認是 leading = false, trailing = true。也就意味着,默認在延遲結束後調用 func 函數
// 若是 沒有傳入 wait, 且 wait 不等於 0, 且瀏覽器支持 requestAnimationFrame時
// useRAF 會等於 true, 表示啓用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
// 若是 沒有傳入 func, 直接拋出錯誤
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
// 設置 wait 的默認值爲 0
wait = +wait || 0
// 初始化 options 選項的 默認參數
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
// 處理 maxWait 參數,若是用戶本身定義了 maxWait, 則和 wait 參數比較,取他們的最大值
// 這裏是爲了防止,用戶 傳入的 wait 大於 maxWait 的狀況
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 封裝執行函數, 用於當即執行 func
function invokeFunc(time) {
// 參數
const args = lastArgs
// 做用域
const thisArg = lastThis
// 重置
lastArgs = lastThis = undefined
// 記錄 func 函數執行時的時間戳
lastInvokeTime = time
// 執行函數
result = func.apply(thisArg, args)
return result
}
// 開啓定時器
function startTimer(pendingFunc, wait) {
if (useRAF) {
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 清除定時器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
// 在延遲開始前調用
// 對 invokeFunc 的封裝,返回 invokeFunc 函數的返回值
function leadingEdge(time) {
// 記錄 函數被調用時 的時間戳
lastInvokeTime = time
// 開啓一個定時器
timerId = startTimer(timerExpired, wait)
// 若是 leading 爲 true,則表示須要在延遲開始前 先執行一次 func 函數
return leading ? invokeFunc(time) : result
}
// 在延遲結束後調用
// 對 invokeFunc 的封裝,返回 invokeFunc 函數的返回值
function trailingEdge(time) {
timerId = undefined
// 這裏加了個 lastArgs 的判斷,lastArgs 會在 debounced 函數執行時賦值
if (trailing && lastArgs) {
return invokeFunc(time)
}
// 重置 參數和 做用域
lastArgs = lastThis = undefined
return result
}
//
function remainingWait(time) {
// 計算 time 與最近一次調用 debounced 函數的時間差
const timeSinceLastCall = time - lastCallTime
// 計算 time 與最近一次調用 func 函數的時間差
const timeSinceLastInvoke = time - lastInvokeTime
// 用 wait 減去已經等待的時間
const timeWaiting = wait - timeSinceLastCall
return maxing
// 若是設置了最大等待時長,
// 則須要 比較 ( wait 減去已經等待的時間 ) 和 ( maxWait 減去已經等待的時間 ),取最小值
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
// 不然直接返回 wait 減去已經等待的時間
: timeWaiting
}
// 根據時間判斷是否能夠執行函數
function shouldInvoke(time) {
// 計算 time 與最近一次調用 debounced 函數的時間差
const timeSinceLastCall = time - lastCallTime
// 計算 time 與最近一次調用 func 函數的時間差
const timeSinceLastInvoke = time - lastInvokeTime
return (
// 判斷是否是第一次執行 debouned 函數,若是是第一次執行,確定能夠調用 func 函數
lastCallTime === undefined
// 判斷距離最近一次調用 debounced 函數的時間差,是否大於等於 wait,若是是的話,也就意味着能夠調用 func 函數
|| (timeSinceLastCall >= wait)
// 正常狀況 timeSinceLastCall 不會小於 0, 除非手動調整了系統時間
|| (timeSinceLastCall < 0)
// 若是設置了 maxWait,判斷距離上一次調用 func 函數的時間差,是否超過了最大等待時長
|| (maxing && timeSinceLastInvoke >= maxWait))
}
// 封裝執行函數,用於 wait 延遲結束後執行
function timerExpired() {
const time = Date.now()
// 根據時間來判斷是否能夠執行 func 函數
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 從新計算時間,從新建一個定時器
timerId = startTimer(timerExpired, remainingWait(time))
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
// 若是能夠執行 func。第一次執行的時候,isInvoking 確定是 true
if (isInvoking) {
// 第一次執行時
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
// 若是能夠執行 func,且又有 timerId 。說明 func 能夠執行了,但是又沒有執行 trailingEdge
// 若是又已經設置了 maxWait,就當即執行 func
// 何時會出現這種狀況, 我還不明白。
if (maxing) {
// 開啓一個定時器
timerId = startTimer(timerExpired, wait)
// 當即執行 func 函數
// 爲何設置 maxWait 就須要理解執行 func ,下面分析 throttle 的時候就明白了
return invokeFunc(lastCallTime)
}
}
// 什麼狀況下 不是第一次執行, 卻又沒有 timerId 呢?
// 由於 trailingEdge 函數內部會執行 timerId = undefined
// 若是恰好 trailingEdge 函數執行以後,又觸發了 debounced ,就會出現這種狀況
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
// 返回一個閉包
return debounced
}
複製代碼
注意:
lastCallTime
存儲的是 debounced
函數執行時的時間戳。
lastInvokeTime
存儲的是 func
函數執行時的時間戳。
lodash
的 debounced
還提供了3個api
,供外部調用
// 取消 debounce
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
// 執行 func
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
// 判斷是否正在等待中
function pending() {
return timerId !== undefined
}
// 暴露出三個方法
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
複製代碼
throttle
其實就是設置了 leading
和 maxWait
的 debounce
。
function throttle(func, wait, options) {
let leading = true
let trailing = true
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
// 初始化 leading 和 trailing 的默認值
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 默認狀況下,leading 爲 true, trailing 爲 true。
// 表示 在延遲開始前,和延遲結束後,都須要調用 func
// 還傳入了一個 maxWait
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
})
}
複製代碼
debounce
的源碼,我沒有徹底看明白,有幾個地方是個人猜想。好比 debounced
函數內的 if (maxing)
,不明白爲何要這樣判斷。
很是但願有明白的道友,告訴下我。
若是有錯誤的地方,還請指出。
謝謝閱讀。