lodash防抖節流源碼理解

用過lodash庫的應該熟悉_.debounce_.throttle,也就是函數防抖(debounce)和節流(throttle)。javascript

  • 函數防抖和節流分別是什麼?
  • 爲何《JavaScript高級程序設計》中throttle函數和防抖寫法同樣?
  • lodash庫的debounce和throttle是什麼關係,爲何throttle返回的是debounce的執行結果?

概念

關於防抖動(debounce),能夠從接點彈跳(bounce)瞭解起:css

接點彈跳(bounce)是一個在機械開關與繼電器上廣泛的問題。開關與繼電器的接點一般由彈性金屬製成。當接點一塊兒敲擊,接點在衝力與彈力一塊兒做用下,致使彈跳部分在接點穩定前發生一次或屢次。排除接點彈跳效應的方法就是所謂的"去彈跳"(debouncing)電路。java

由此衍生"去彈跳"(debounce)一詞,出現於軟件開發工業中,用來描述一個消除開關彈跳實施方法的比率限制或是頻率調節。git

關於節流閥(throttle):github

節流閥是一個能夠調節流體壓力的構造,可調整進入引擎的流量,進而調整引擎的出力。瀏覽器

衆所周知,JavaScript是單線程做業,原本就至關忙碌,就不能總是堵着人家,嚴重的話可能致使瀏覽器掛起甚至崩潰。要避免高頻的調用昂貴的計算操做,好比DOM交互。app

防抖和節流都能作到優化函數執行效率的效果,是很類似的技術。再加上高程中throttle函數和理解中的防抖一致,因此很容易讓人混淆。查閱資料的時候很多人都表示高程上throttle函數實現的其實是防抖動,函數命名錯誤。可是換種理解:防抖和節流都屬於節流技術,它們的基本思想是同樣的:函數

函數節流背後的基本思想是指,某些代碼不能夠在沒有間斷的狀況連續重複執行。(《JavaScript高級程序設計》)優化

高程原文的重點在於setTimeout優化函數執行頻率,這種狀況下不用去刻意區分防抖和節流的區別。ui

函數防抖

規定間隔不超過n毫秒的連續調用內只有一次調用生效。

咱們來模擬文本輸入搜索:

function searchAjax(query) {
    console.log(`Results for "${query}"`);
}
document.getElementById("searchInput").addEventListener("input", function(event) {
    searchAjax(event.target.value);
});
複製代碼

運行上面一段代碼:

搜索「珍珠奶茶」

在用戶輸入結束以前,input事件調用了15次搜索請求。實際上,只有字符輸入結束那一刻的input事件的搜索請求才是有用的(中文輸入法尤其明顯),其他都是在白白浪費資源。

接下來看一下添加防抖以後的效果:

function debounce(func, wait) {
    let timeId;
    return function(...args) {
    	let _this = this;
        clearTimeout(timeId);
        timeId = setTimeout(function() {
            func.apply(_this, args);
        }, wait);
    }
}
let searchDebounce = debounce(function(query) {
    console.log(`Results for "${query}"`);
}, 500);
document.getElementById("searchInput").addEventListener("input", function(event) {
    searchDebounce(event.target.value);
});
複製代碼

運行結果以下:

防抖效果

用戶輸入結束前,不會再頻繁的調用搜索請求了,只保留最關鍵的一次「珍珠奶茶」搜索。

debounce函數接收searchAjax方法,並將它包裝成擁有防抖能力的searchDebounce方法:設置調用延時爲0.5s,若是延時過程當中searchDebounce被再次調用,從新延時0.5s,直至有明顯的停頓(searchDebounce沒有再次被調用),這纔開始處理搜索請求(searchAjax)。

函數被調用後延遲n毫秒執行,在等待過程當中若是函數再次被調用,從新計時。

函數防抖(trailing)

leading和trailing

在文本輸入搜索的例子中,搜索函數被延遲執行。不過並不是全部處理都須要延遲執行,比方說:submit按鈕,用戶更但願提交動做在第一時間完成,而且能避免短期內的重複提交。

函數被調用後當即執行,而且在將來n毫秒內不重複執行,若是停頓過程當中函數被再次調用,從新計時。

函數防抖(leading)

根據函數執行的前後順序,出現兩種選項:先執行後等待爲leading,先等待後執行爲trailing

函數節流

規定n秒內函數只有一次調用生效。

以滾動加載內容爲例:

頁面滾動過程當中勢必會高頻觸發scroll事件(滾動500px能夠觸發100+次scroll事件),若是每一個scroll事件都須要處理昂貴的計算,那麼整個滾動體驗可能會致使心態崩裂。

無限滾動過程當中debounce也起不了做用,由於只有明顯的停頓debounce纔會處理scroll事件。用戶更但願滾動過程流暢無感知,這須要咱們以合理的頻率不斷檢查是否須要加載更多的內容。

function throttle(func, wait) {
    let lastTime, deferTimer;
    return function(...args) {
        let _this = this;
        let currentTime = Date.now();
        if (!lastTime || currentTime >= lastTime + wait) {
        	lastTime = currentTime;
            func.apply(_this, args);
        } else {
            clearTimeout(deferTimer);
            deferTimer = setTimeout(function() {
        		lastTime = currentTime;
                func.apply(_this, args);
            }, wait);
        }
    }
}

function addContent() {/*...*/}
$(document).on("scroll", throttle(addContent, 300));
複製代碼

與debounce函數同樣,throttle一樣將addContent函數封裝成帶有節流功能的函數:設置延時時間爲0.3s,每0.3s調用一次func函數。若是不知足調用的時間條件,使用定時器預留一次func調用備用,即便最終沒達到0.3s,調用也能生效一次。

lodash源碼解讀

debounce

源碼地址

爲方便理解,如下是簡化過的版本:

function debounce(func, wait, options) {
  let lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime

  let lastInvokeTime = 0
  let leading = false
  let maxing = false
  let trailing = true

  // 初始化
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  wait = +wait || 0
  if (isObject(options)) {
    leading = !!options.leading
    maxing = 'maxWait' in options
    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
    lastInvokeTime = time
    result = func.apply(thisArg, args)
    return result
  }

  // 啓動延時
  function startTimer(pendingFunc, wait) {
    return setTimeout(pendingFunc, wait)
  }

  // 延時開始前
  function leadingEdge(time) {
    lastInvokeTime = time
    // 啓動延時
    timerId = startTimer(timerExpired, wait)
    // 若是是leading模式,延時前調用func
    return leading ? invokeFunc(time) : result
  }

  //計算剩餘的延時時間:
  //1. 不存在maxWait:(上一次debouncedFunc調用後)延時不能超過wait
  //2. 存在maxWait:func調用不能被延時超過maxWait
  //根據這兩種狀況計算出最短期
  function remainingWait(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    const timeWaiting = wait - timeSinceLastCall

    return maxing
      ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting
  }

  //判斷當前時間是否能調用func:
  //1.首次調用debouncedFunc
  //2.距離上一次debouncedFunc調用後已延遲wait毫秒
  //3.func調用總延遲達到maxWait毫秒
  //4.系統時間倒退
  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCallTime
    const timeSinceLastInvoke = time - lastInvokeTime
    return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
      (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
  }

  // 延時器回調
  function timerExpired() {
    const time = Date.now()
    // 若是知足時間條件,結束延時
    if (shouldInvoke(time)) {
      return trailingEdge(time)
    }
    // 沒知足時間條件,計算剩餘等待時間,繼續延時
    timerId = startTimer(timerExpired, remainingWait(time))
  }

  //延時結束後
  function trailingEdge(time) {
    timerId = undefined
    //若是是trailing模式,調用func
    if (trailing && lastArgs) {
      return invokeFunc(time)
    }
    lastArgs = lastThis = undefined
    return result
  }

  //debouncedFunc
  function debounced(...args) {
    const time = Date.now()
    const isInvoking = shouldInvoke(time)

    lastArgs = args
    lastThis = this
    lastCallTime = time

    if (isInvoking) {
      //timerId不存在有兩種緣由:
      //1. 首次調用
      //2. 上次延時調用結束
      if (timerId === undefined) {
        return leadingEdge(lastCallTime)
      }
      // 存在func調用最長延時限制時,執行func並啓動下一次延時,可實現throttle
      if (maxing) {
        timerId = startTimer(timerExpired, wait)
        return invokeFunc(lastCallTime)
      }
    }
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait)
    }
    return result
  }
  return debounced
}

export default debounce
複製代碼

提示:理解debounce的時候能夠暫時忽略maxWait,後面會解釋maxWait的做用。

debouncedFunc是如何工做的:

  1. 首次調用

    執行leadingEdge函數,leading選項爲true時表示在延時以前調用func,而後啓動延時器。延時器的做用是:在延時結束以後執行trailingEdge函數,trailing選項爲true時表示在延遲結束以後調用func,最終結束一次func調用延遲的過程。

  2. 再次調用

    若是上一次的func延時調用已經結束,再次執行leadingEdge函數來啓動延時過程。不然,忽略這次調用。(若是設置了maxWait且當前知足調用的時間條件,那麼當即調用func而且啓動新的延時器)

若是leading和trailing選項同時爲true,那麼func在一次防抖過程能被調用屢次。

lodash在debounce的基礎上添加了maxWait選項,用於規定func調用不能延遲超過maxWait毫秒,也就是說每段maxWait時間內func必定會被調用一次。因此只要設置了maxWait選項,那麼效果就等同於函數節流了。這一點也能夠經過lodash的throttle源碼獲得驗證: throttle的wait做爲debounce的maxWait傳入。

throttle

源碼地址

function throttle(func, wait, options) {
  let leading = true
  let trailing = true

  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }
  return debounce(func, wait, {
    leading,
    trailing,
    'maxWait': wait
  })
}

export default throttle
複製代碼

參考資料:

《JavaScript高級程序設計》

Debouncing and Throttling Explained Through Examples

The Difference Between Throttling and Debouncing

相關文章
相關標籤/搜索