101
_.debounce(func, [wait=0], [options={}])
jQuery(window).on('resize', debounce(calculateLayout, 150))
jQuery(element).on('click', debounce(sendMail, 300, { 'leading': true, 'trailing': false }))
const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) const source = new EventSource('/stream') jQuery(source).on('message', debounced)
jQuery(window).on('popstate', debounced.cancel)
檢查是否調用處於等待狀態
const status = debounced.pending() ? "Pending..." : "Ready"
參數
func (Function): 需要被去抖的函數
[wait=0] (number): 推遲執行的毫秒數,如果省略這個參數,requestAnimationFrame會被使用
[options={}] (Object): 配置對象
[options.leading=false] (boolean): 是否在wait時延之前調用
[options.maxWait] (number): 在func被調用之前的最大延遲時間
[options.trailing=true] (boolean): 是否在wait時延過后調用
返回值
(Function): 返回新的被去抖的函數
例子
// Avoid costly calculations while the window size is in flux. jQuery(window).on('resize', _.debounce(calculateLayout, 150)); // Invoke `sendMail` when clicked, debouncing subsequent calls. jQuery(element).on('click', _.debounce(sendMail, 300, { 'leading': true, 'trailing': false })); // Ensure `batchLog` is invoked once after 1 second of debounced calls. var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 }); var source = new EventSource('/stream'); jQuery(source).on('message', debounced); // Cancel the trailing debounced invocation. jQuery(window).on('popstate', debounced.cancel);
去抖流程:
首次進入函數時因為 lastCallTime === undefined 並且 timerId === undefined,所以會執行 leadingEdge,如果此時 leading 為 true 的話,就會執行 func。同時,這里會設置一個定時器,在等待 wait(s) 后會執行 timerExpired,timerExpired 的主要作用就是觸發 trailing。
如果在還未到 wait 的時候就再次調用了函數的話,會更新 lastCallTime,並且因為此時 isInvoking 不滿足條件,所以這次什么也不會執行。
時間到達 wait 時,就會執行我們一開始設定的定時器timerExpired,此時因為time-lastCallTime < wait,所以不會執行 trailingEdge。
這時又會新增一個定時器,下一次執行的時間是 remainingWait,這里會根據是否有 maxwait 來作區分:
如果沒有 maxwait,定時器的時間是 wait - timeSinceLastCall,保證下一次 trailing 的執行。
如果有 maxing,會比較出下一次 maxing 和下一次 trailing 的最小值,作為下一次函數要執行的時間。
最后,如果不再有函數調用,就會在定時器結束時執行 trailingEdge。
下面是流程圖片描述:
節流與去抖的區別
源代碼:
import isObject from './isObject.js' import root from './.internal/root.js' /** * Creates a debounced function that delays invoking `func` until after `wait` * milliseconds have elapsed since the last time the debounced function was * invoked, or until the next browser frame is drawn. The debounced function * comes with a `cancel` method to cancel delayed `func` invocations and a * `flush` method to immediately invoke them. Provide `options` to indicate * whether `func` should be invoked on the leading and/or trailing edge of the * `wait` timeout. The `func` is invoked with the last arguments provided to the * debounced function. Subsequent calls to the debounced function return the * result of the last `func` invocation. * * **Note:** If `leading` and `trailing` options are `true`, `func` is * invoked on the trailing edge of the timeout only if the debounced function * is invoked more than once during the `wait` timeout. * * If `wait` is `0` and `leading` is `false`, `func` invocation is deferred * until the next tick, similar to `setTimeout` with a timeout of `0`. * * If `wait` is omitted in an environment with `requestAnimationFrame`, `func` * invocation will be deferred until the next frame is drawn (typically about * 16ms). * * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) * for details over the differences between `debounce` and `throttle`. * * @since 0.1.0 * @category Function * @param {Function} func The function to debounce. * @param {number} [wait=0] * The number of milliseconds to delay; if omitted, `requestAnimationFrame` is * used (if available). * @param {Object} [options={}] The options object. * @param {boolean} [options.leading=false] * Specify invoking on the leading edge of the timeout. * @param {number} [options.maxWait] * The maximum time `func` is allowed to be delayed before it's invoked. * @param {boolean} [options.trailing=true] * Specify invoking on the trailing edge of the timeout. * @returns {Function} Returns the new debounced function. * @example * * // Avoid costly calculations while the window size is in flux. * jQuery(window).on('resize', debounce(calculateLayout, 150)) * * // Invoke `sendMail` when clicked, debouncing subsequent calls. * jQuery(element).on('click', debounce(sendMail, 300, { * 'leading': true, * 'trailing': false * })) * * // Ensure `batchLog` is invoked once after 1 second of debounced calls. * const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) * const source = new EventSource('/stream') * jQuery(source).on('message', debounced) * * // Cancel the trailing debounced invocation. * jQuery(window).on('popstate', debounced.cancel) * * // Check for pending invocations. * const status = debounced.pending() ? "Pending..." : "Ready" */ //創建一個去抖函數來推遲調用func,自從上一次去抖函數被調用之后等待wait毫秒時間過后再調用,或者等待直到下一次瀏覽器幀被重新繪制。創建去抖函數的同時也會創建一個cancel方法去取消延遲func調用,還有一個flush方法來立即調用。也提供了option參數來表明func函數是否應該在等待wait時間開始之前調用還是wait時間過后調用。func函數調用會帶着提供給去抖函數的最后一個配置參數。之后對於去抖函數的調用返回最后一次func調用的結果。 //注意:如果leading和trailing配置項是true,func只有在去抖函數等待wait時間度過的時候被調用超過一次時才會在wait時延過后再次調用。 //如果wait等待時間是0,並且leading是false,func的調用會推遲至下一次事件循環,與setTimeout設置延遲時間為0一樣 //如果wait參數在使用requestAnimationFrame的環境中被省略,func會延遲至下一次頁面重繪的時候被調用,通常延遲16ms //使用場景 //當window大小不斷變化的時候避免昂貴的計算,例如jQuery(window).on('resize', debounce(calculateLayout, 150)) //當點擊的時候調用`sendMail`,將隨后的調用去抖 // jQuery(element).on('click', debounce(sendMail, 300, { // 'leading': true, // 'trailing': false // })) //確保`batchLog`在去抖化調用后1秒鍾被調用1次 // const debounced = debounce(batchLog, 250, { 'maxWait': 1000 }) // const source = new EventSource('/stream') // jQuery(source).on('message', debounced) //取消wait時延過后的去抖調用 //jQuery(window).on('popstate', debounced.cancel) //檢查是否調用處於等待狀態 //const status = debounced.pending() ? "Pending..." : "Ready" //func是需要去抖的方法 //wait推遲執行的毫秒數,如果省略這個參數,requestAnimationFrame會被使用 //requestAnimationFrame瀏覽器在下一次重繪之前調用指定的函數 //options中的配置項 //options.leading在wait時延之前調用 //options.trailing在wait時延過后調用 //options.maxWait在func被調用之前的最大延遲時間 function debounce(func, wait, options) { let lastArgs, lastThis, maxWait,//func調用之前的最大延遲時間 result, timerId,//定時器 lastCallTime let lastInvokeTime = 0//上一次調用func的時間 let leading = false//是否在wait時延之前調用 let maxing = false//是否設置了func調用之前的最大延遲時間 let trailing = true//是否在wait時延過后調用 // Bypass `requestAnimationFrame` by explicitly setting `wait=0`. //忽略requestAnimationFrame通過明確地設置wait=0 const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function') if (typeof func != 'function') {//如果func不是function類型,拋出錯誤 throw new TypeError('Expected a function') } wait = +wait || 0//將wait轉換成數字 if (isObject(options)) {//如果options是對象 leading = !!options.leading//是否在wait時延之前調用 maxing = 'maxWait' in options//是否設置了func調用之前的最大延遲時間 maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait//func調用之前的最大延遲時間 trailing = 'trailing' in options ? !!options.trailing : trailing//是否在wait時延過后調用 } function invokeFunc(time) {//執行func const args = lastArgs const thisArg = lastThis lastArgs = lastThis = undefined//debounced接收參數和this置空 lastInvokeTime = time//更新上一次執行func時間 result = func.apply(thisArg, args)//調用func獲取結果返回 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) } function leadingEdge(time) {//leading時調用 // Reset any `maxWait` timer. lastInvokeTime = time//重置上一次func執行時間 // Start the timer for the trailing edge. timerId = startTimer(timerExpired, wait)//為wait時延過后的trailing調用開啟定時器 // Invoke the leading edge. return leading ? invokeFunc(time) : result//如果leading為true,wait時延之前調用,直接調用,否則返回result,undefined } function remainingWait(time) {//還剩下多少時間可以執行下一次 const timeSinceLastCall = time - lastCallTime//距離上次去抖時間 const timeSinceLastInvoke = time - lastInvokeTime//距離上次func執行時間 const timeWaiting = wait - timeSinceLastCall//距離上次去抖時間還需要等多久,下一次trailing時間 return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting //如果有傳遞最大等待時間,就在下一次trailing時間和maxWait - timeSinceLastInvoke里找最小的 //如果沒有傳遞最大等待時間,返回下一次trailing時間 } function shouldInvoke(time) {//判斷在time時間點是否func應該被調用 const timeSinceLastCall = time - lastCallTime//距離上次去抖時間 const timeSinceLastInvoke = time - lastInvokeTime//距離上次func執行時間 // Either this is the first call, activity has stopped and we're at the // trailing edge, the system time has gone backwards and we're treating // it as the trailing edge, or we've hit the `maxWait` limit. return (lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait)) //lastCallTime === undefined首次調用 //timeSinceLastCall >= wait距離上次被去抖已經超過 wait //timeSinceLastCall < 0//系統時間倒退 //maxing && timeSinceLastInvoke >= maxWait//距離上次func執行時間已經超過最大等待時間 } function timerExpired() {//等待wait時間后觸發trailing const time = Date.now()//當前時間 if (shouldInvoke(time)) {//如果當前時間應該調用,就調用trailingEdge判斷是否已經去抖過一次 return trailingEdge(time) } // Restart the timer. timerId = startTimer(timerExpired, remainingWait(time))//如果此刻不能執行func,就重新啟動定時器,時間為剩余等待時間 } function trailingEdge(time) {//trailing時調用func timerId = undefined//定時器置空 // Only invoke if we have `lastArgs` which means `func` has been // debounced at least once. if (trailing && lastArgs) {//只有在擁有lastArgs參數的時候才執行func,lastArgs說明func被去抖至少一次 return invokeFunc(time) } lastArgs = lastThis = undefined//lastArgs和lastThis置空 return result//返回結果undefined } function cancel() {//取消定時器,並將上一次調用時間清0,上一次debounced接收參數,this,上一次去抖時間都置空 if (timerId !== undefined) { cancelTimer(timerId) } lastInvokeTime = 0 lastArgs = lastCallTime = lastThis = timerId = undefined } function flush() {//如果沒有定時器,返回當前結果,如果有定時器,調用trailingEdge return timerId === undefined ? result : trailingEdge(Date.now()) } function pending() {//查看當前是否處於等待狀態 return timerId !== undefined } function debounced(...args) { const time = Date.now()//當前時間毫秒數 const isInvoking = shouldInvoke(time)//判斷當前時間是否func應該被調用 lastArgs = args//參數數組 lastThis = this//本次this lastCallTime = time//去抖時間 if (isInvoking) {//如果此刻需要被調用,1說明是首次調用且是leading調用 2距離上次去抖wait時間了 3距離上次執行時間過去了最大等待時間 if (timerId === undefined) {//如果沒有定時器說明1首次調用2剛執行過取消操作或者trailing調用 return leadingEdge(lastCallTime)//調用leadingEdge判斷是否leading時需要調用 } if (maxing) {//如果不是第一次調用且設置了最大延遲時間,說明已經超過了最大延遲時間,直接調用返回結果 // Handle invocations in a tight loop. timerId = startTimer(timerExpired, wait)//開啟一個定時器,等待時間wait,判斷是否wait時間后需要出發trailing調用 return invokeFunc(lastCallTime)//調用func返回結果 } } if (timerId === undefined) {//如果此刻不需要被調用,且定時器沒有開啟,就開啟一個定時器,等待時間wait timerId = startTimer(timerExpired, wait) } return result } debounced.cancel = cancel debounced.flush = flush debounced.pending = pending return debounced } export default debounce