從lodash源碼學習節流與防抖

  以前遇到過一個場景,頁面上有幾個d3.js繪製的圖形。若是調整瀏覽器可視區大小,會引起圖形重繪。當圖中的節點比較多的時候,頁面會顯得異常卡頓。爲了限制相似於這種短期內高頻率觸發的狀況,咱們可使用防抖函數。javascript

  實際開發過程當中,這樣的狀況其實不少,好比:java

  • 頁面的scroll事件
  • input框等的輸入事件
  • 拖拽事件用到的mousemove等

  先說說防抖和節流是個啥,有啥區別瀏覽器

防抖:設定一個時間間隔,當某個頻繁觸發的函數執行一次後,在這個時間間隔內不會再次被觸發,若是在此期間嘗試觸發這個函數,則時間間隔會從新開始計算。

節流:設定一個時間間隔,某個頻繁觸發的函數,在這個時間間隔內只會執行一次。也就是說,這個頻繁觸發的函數會以一個固定的週期執行。閉包

debounce(函數防抖)

  大體捋一遍代碼結構。爲了方便閱讀,咱們先把源碼中的Function註釋掉。app

function debounce(func, wait, options) {
    // 代碼一開始,以閉包的形式定義了一些變量
      var lastArgs,  //  最後一次debounce的arguments,它其實起一個標記位的做用,後面會提到
          lastThis,  //  就是last this,用來修正this指向
          maxWait,   //  存儲option裏面傳入的maxWait值,最大等待時間
          result,    //  其實這個result始終都是undefined
          timerId,   // setTimeout賦給它,用於表示當前定時器
          lastCallTime,   // 最後一次調用debounce的時刻
          lastInvokeTime = 0,    //  最後一次調用用戶傳入函數的時刻
          leading = false,   //  是否在一開始就執行用戶傳入的函數
          maxing = false,    //  是否有最大等待時間
          trailing = true;   //  是否在等待週期結束後執行用戶傳入的函數

    //  用戶傳入的fun必須是個函數,不然報錯
      if (typeof func != 'function') {
        throw new TypeError(FUNC_ERROR_TEXT);
      }
      
    //  toNumber是lodash封裝的一個轉類型的方法
      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;
      }

    //  執行用戶傳入的函數
      function invokeFunc(time) {
        // ......
      }

    //  防抖開始時執行的操做
      function leadingEdge(time) {)
        // ......
      }

    //  計算仍然須要等待的時間
      function remainingWait(time) {
        // ......
      }

    //  判斷此時是否應該執行用戶傳入的函數
      function shouldInvoke(time) {
        // ......
      }

    //  等待時間結束後的操做
      function timerExpired() {
        // ......
      }

    //  執行用戶傳入的函數
      function trailingEdge(time) {
        // ......
      }

    //  取消防抖
      function cancel() {
        // ......
      }

     //  當即執行用戶傳入的函數
      function flush() {
        // ......
      }

    // 防抖開始的入口
      function debounced() {
        // ......
      }
      
      
      debounced.cancel = cancel;
      debounced.flush = flush;
      return debounced;
    }

咱們先從入口函數開始。函數開始執行後,首先會出現三種狀況:函數

  • 時間上達到了能夠執行的條件;
  • 時間上不知足條件,可是此時的定時器並無啓動;
  • 不知足條件,返回undefined

  說實話,第二種狀況沒想到場景,哪位大佬給補充一下呢。工具

  代碼中timerId = setTimeout(timerExpired, wait);是用來設置定時器,到時間後觸發trailingEdge這個函數。oop

function debounced() {
        var time = now(),
            isInvoking = shouldInvoke(time);   // 判斷此時是否能夠開始執行用戶傳入的函數
        
        lastArgs = arguments;
        lastThis = this;
        lastCallTime = time;

        if (isInvoking) {
          // 若是此時並無定時器存在,就開始進入防抖階段
          if (timerId === undefined) {
            return leadingEdge(lastCallTime);
          }
          //  若是設置了最大等待時間,便當即執行用戶傳入的函數
          if (maxing) {
            // Handle invocations in a tight loop.
            timerId = setTimeout(timerExpired, wait);
            return invokeFunc(lastCallTime);
          }
        }
        if (timerId === undefined) {
          timerId = setTimeout(timerExpired, wait);
        }
        
        //  不知足條件,return undefined
        return result;
      }

  咱們先來看看shouldInvoke是如何判斷函數是否能夠執行的。this

function shouldInvoke(time) {
        //  lastCallTime初始值是undefined,lastInvokeTime初始值是0,
        //  防抖函數被手動取消後,這兩個值會被設爲初始值
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime;

        // 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) ||  //  上次調用時刻距離如今已經大於wait值
                (timeSinceLastCall < 0) ||      //  當前時間-上次調用時間小於0,應該只多是手動修改了系統時間吧
                (maxing && timeSinceLastInvoke >= maxWait)  //  設置了最大等待時間,且已超時
            );
      }

  咱們繼續分析函數開始的階段leadingEdge。首先重置防抖函數最後調用時間,而後去觸發一個定時器,保證wait後接下來的執行。最後判斷若是leadingtrue的話,當即執行用戶傳入的函數:code

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;
      }

  咱們已經不止一次去設定觸發器了,來咱們探究一下里面到底作了啥。其實很簡單,判斷時間是否符合執行條件,符合的話觸發trailingEdge,也就是後續操做,不然計算須要等待的時間,並從新調用這個函數,其實這裏就是防抖的核心所在了。

function timerExpired() {
        var time = now();
        if (shouldInvoke(time)) {
          return trailingEdge(time);
        }
        // Restart the timer.
        timerId = setTimeout(timerExpired, remainingWait(time));
      }

  至於如何從新計算剩餘時間的,這裏不做過多解釋,你們一看便知。

function remainingWait(time) {
        var timeSinceLastCall = time - lastCallTime,
            timeSinceLastInvoke = time - lastInvokeTime,
            timeWaiting = wait - timeSinceLastCall;

        return maxing
          ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
          : timeWaiting;
      }

  咱們說說等待時間到了之後的操做。重置了一些本週期的變量。而且,若是trailingtrue並且lastArgs存在時,纔會再次執行用戶傳入的參數。這裏解釋了文章開頭提到的lastArgs只是個標記位,如註釋所說,他表示debounce至少執行了一次。

function trailingEdge(time) {
        timerId = undefined;

        // Only invoke if we have `lastArgs` which means `func` has been
        // debounced at least once.
        if (trailing && lastArgs) {
          return invokeFunc(time);
        }
        lastArgs = lastThis = undefined;
        return result;
      }

  執行用戶傳入的函數比較簡單,咱們知道callapply是會當即執行的,其實最後的result仍是undefined

function invokeFunc(time) {
        var args = lastArgs,
            thisArg = lastThis;
        //  重置了一些條件
        lastArgs = lastThis = undefined;
        lastInvokeTime = time;
        //  執行用戶傳入函數
        result = func.apply(thisArg, args);
        return result;
      }

  最後就是取消防抖和當即執行用戶傳入函數的過程了,代碼一目瞭然,不做過多解釋。

function cancel() {
        if (timerId !== undefined) {
          clearTimeout(timerId);
        }
        lastInvokeTime = 0;
        lastArgs = lastCallTime = lastThis = timerId = undefined;
      }

      function flush() {
        return timerId === undefined ? result : trailingEdge(now());
      }

throttle(函數節流)

  節流其實原理跟防抖是同樣的,只不過觸發條件不一樣而已,其實就是maxWaitwait的防抖函數。

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,
        'maxWait': wait,
        'trailing': trailing
      });
    }

總結

  咱們發現,其實lodash除了在cancle函數中使用了清除定時器的操做外,其餘地方並無去關心定時器,而是很巧妙的在定時器里加了一個判斷條件來判斷後續函數是否能夠執行。這就避免了手動管理定時器。

  lodash替咱們考慮到了一些比較少見的情景,並且還有必定的容錯性。即使ES6實現了不少目前經常使用的工具函數,可是面對複雜的情景,咱們依然能夠以按需引入的方式使用lodash的一些函數來提高開發效率,同時使得咱們的程序更加健壯。

相關文章
相關標籤/搜索