【跟著大佬學JavaScript】之lodash防抖節流合併

易函123發表於2022-07-10

前言

前面已經對防抖和節流有了介紹,這篇主要看lodash是如何將防抖和節流合併成一個函式的。

初衷是深入lodash,學習它內部的好程式碼並應用,同時也加深節流防抖的理解。這裡會先從防抖開始一步步往後,由簡入繁,直到最後實現整個函式。

這裡純粹自己的理解,以及看了很多篇優質文章,希望能加深對節流防抖的理解,如果有不同意見或者看法,歡迎大家評論。

原理

前面雖然已經介紹過防抖和節流原理,這裡為了加深印象,再搬過來。

防抖的原理:在wait時間內,持續觸發某個事件。第一種情況:如果某個事件觸發wait秒內又觸發了該事件,就應該以新的事件wait等待時間為準,wait秒後再執行此事件;第二種情況:如果某個事件觸發wait秒後,未再觸發該事件,則在wait秒後直接執行該事件。

通俗點說:定義wait=3000,持續點選按鈕,前後點選間隔都在3秒內,則在最後一次點選按鈕後,等待3秒再執行func方法。如果點選完按鈕,3秒後未再次點選按鈕,則3秒後直接執行func方法。

節流的原理:持續觸發某事件,每隔一段時間,只執行一次。

通俗點說,3 秒內多次呼叫函式,但是在 3 秒間隔內只執行一次,第一次執行後 3 秒 無視後面所有的函式呼叫請求,也不會延長時間間隔。3 秒間隔結束後則開始執行新的函式呼叫請求,然後在這新的 3 秒內依舊無視後面所有的函式呼叫請求,以此類推。

簡單來說:每隔單位時間( 3 秒),只執行一次。

程式碼分析

一、引入程式碼部分

首先看原始碼最前方的引入。

import isObject from './isObject.js'
import root from './.internal/root.js'

isObject方法,直接拿出來,

function isObject(value) {
    const type = typeof value;
    return value != null && (type === "object" || type === "function");
}

root的引入主要是window。為了引出window.requestAnimationFrame

二、requestAnimationFrame程式碼

window.requestAnimationFrame()告訴瀏覽器希望執行動畫並請求瀏覽器在下一次重繪之前呼叫指定的函式來更新動畫,差不多 16ms 執行一次。

lodash這裡使用requestAnimationFrame,主要是使用者使用debounce函式未設定wait的情況下使用requestAnimationFrame

const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
function startTimer(pendingFunc, wait) {
    if (useRAF) {
        window.cancelAnimationFrame(timerId)
        return window.requestAnimationFrame(pendingFunc)
    }
    return setTimeout(pendingFunc, wait)
}

function cancelTimer(id) {
    if (useRAF) {
        return window.cancelAnimationFrame(id)
    }
    clearTimeout(id)
}

由程式碼const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')不難看出,函式未傳入wait並且window.cancelAnimationFrame函式存在這兩種情況下操作window.requestAnimationFrame

三、由簡入繁輸出防抖函式

  • 首先,我們來看下lodash debounce API
    這部分引數內容就直接摘抄在下方:

    • func (Function): 要防抖動的函式。
    • [wait=0] (number): 需要延遲的毫秒數。
    • [options=] (Object): 選項物件。
    • [options.leading=false] (boolean): 指定在延遲開始前呼叫。
    • [options.maxWait] (number): 設定 func 允許被延遲的最大值。
    • [options.trailing=true] (boolean): 指定在延遲結束後呼叫。
  • 然後,我們一般防抖函式,需要的引數是:funcwaitimmediate這三個引數,對應lodash,我們需要拿出這四個部分:

    • func (Function): 要防抖動的函式。
    • [wait=0] (number): 需要延遲的毫秒數。
    • [options=] (Object): 選項物件。
    • [options.leading=false] (boolean): 指定在延遲開始前呼叫。
  • 接著,按照這個形式,先寫出最簡防抖方法。也就是這兩部分引數的程式碼

    • func (Function): 要防抖動的函式。
    • [wait=0] (number): 需要延遲的毫秒數。
// 程式碼1
function debounce(func, wait) {
    let timerId, // setTimeout 生成的定時器控制程式碼
      lastThis, // 儲存上一次 this
      lastArgs, // 儲存上一次執行 debounced 的 arguments
      result; // 函式 func 執行後的返回值,多次觸發但未滿足執行 func 條件時,返回 result
    
    wait = +wait || 0; // 等待時間
    
    // 沒傳 wait 時呼叫 window.requestAnimationFrame()
    const useRAF =
      !wait &&
      wait !== 0 &&
      typeof window.requestAnimationFrame === "function";
    
    // 取消debounce
    function cancel() {
      if (timerId !== undefined) {
        cancelTimer(timerId);
      }
      lastArgs = lastThis = timerId = result = undefined;
    }
    
    // 開啟定時器
    // 1.未傳wait時使用requestAnimationFrame
    // 2.直接使用定時器
    function startTimer(pendingFunc, wait) {
      if (useRAF) {
        window.cancelAnimationFrame(timerId);
        return window.requestAnimationFrame(pendingFunc);
      }
      return setTimeout(pendingFunc, wait);
    }
    
    // 定時器回撥函式,表示定時結束後的操作
    function timerExpired(wait) {
      const time = Date.now();
      timerId = startTimer(invokeFunc, wait);
    }
    
    // 取消定時器
    function cancelTimer(id) {
      if (useRAF) {
        return window.cancelAnimationFrame(id);
      }
      clearTimeout(id);
      timerId = undefined;
    }
    
    // 執行函式,並將原函式的返回值result輸出
    function invokeFunc() {
      const args = lastArgs;
      const thisArg = lastThis;
    
      lastArgs = lastThis = undefined; // 清空當前函式指向的this,argumnents
      result = func.apply(thisArg, args); // 繫結當前函式指向的this,argumnents
      return result;
    }
    
    const debounced = function (...args) {
      const time = Date.now(); // 獲取當前時間
    
      lastArgs = args;
      lastThis = this;
      
      if (timerId) {
        cancelTimer(timerId);
      }
      if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait);
      }
    };
    
    debounced.cancel = cancel;
    return debounced;
}
    看上述程式碼:
    1. 多了未傳wait情況,使用`window.requestAnimationFrame`。  
    2. 將定時器,繫結this,arguments、result和取消定時器等分函式拿了出來。
  • 再者,將options的leading加上。也就是immediate立即執行,組成完整的防抖函式。引入引數是下面這部分
    • func (Function): 要防抖動的函式。
    • [wait=0] (number): 需要延遲的毫秒數。
    • [options=] (Object): 選項物件。
    • [options.leading=false] (boolean): 指定在延遲開始前呼叫。
// 程式碼二

function debounce(func, wait, options) {
    let timerId, // setTimeout 生成的定時器控制程式碼
      lastThis, // 儲存上一次 this
      lastArgs, // 儲存上一次執行 debounced 的 arguments
      result, // 函式 func 執行後的返回值,多次觸發但未滿足執行 func 條件時,返回 result
      lastCallTime; // 上一次呼叫 debounce 的時間
    
    let leading = false; // 判斷是否立即執行,預設false
    
    wait = +wait || 0;
    
    // 從options中獲取是否立即執行
    if (isObject(options)) {
      leading = !!options.leading;
    }
    // 沒傳 wait 時呼叫 window.requestAnimationFrame()
    const useRAF =
      !wait &&
      wait !== 0 &&
      typeof window.requestAnimationFrame === "function";
    
    // 取消debounce
    function cancel() {
      if (timerId !== undefined) {
        cancelTimer(timerId);
      }
      lastArgs = lastThis = timerId = result = lastCallTime = undefined;
    }
    
    // 開啟定時器
    function startTimer(pendingFunc, wait) {
      if (useRAF) {
        window.cancelAnimationFrame(timerId);
        return window.requestAnimationFrame(pendingFunc);
      }
      return setTimeout(pendingFunc, wait);
    }
    
    // 定時器回撥函式,表示定時結束後的操作
    function timerExpired(wait) {
      const time = Date.now();
      // 1、是否需要執行
      // 執行事件結束後的那次回撥,否則重啟定時器
      if (shouldInvoke(time)) {
        return trailingEdge(time);
      }
      // 2、否則 計算剩餘等待時間,重啟定時器,保證下一次時延的末尾觸發
      timerId = startTimer(timerExpired, wait);
    }
    
    // 這裡時觸發後仍呼叫函式
    function trailingEdge(time) {
      timerId = undefined;
    
      // 只有當我們有 `lastArgs` 時才呼叫,這意味著`func'已經被呼叫過一次。
      if (lastArgs) {
        return invokeFunc(time);
      }
      lastArgs = lastThis = undefined;
      return result;
    }
    
    // 取消定時器
    function cancelTimer(id) {
      if (useRAF) {
        return window.cancelAnimationFrame(id);
      }
      clearTimeout(id);
    }
    
    function invokeFunc(time) {
      const args = lastArgs;
      const thisArg = lastThis;
    
      lastArgs = lastThis = undefined; // 清空當前函式指向的this,argumnents
      result = func.apply(thisArg, args); // 繫結當前函式指向的this,argumnents
      return result;
    }
    // 判斷此時是否立即執行 func 函式
    // lastCallTime === undefined 第一次呼叫時
    // timeSinceLastCall >= wait 超過超時時間 wait,處理事件結束後的那次回撥
    // timeSinceLastCall < 0 當前時間 - 上次呼叫時間小於 0,即更改了系統時間
    function shouldInvoke(time) {
      const timeSinceLastCall = time - lastCallTime;
      return (
        lastCallTime === undefined ||
        timeSinceLastCall >= wait ||
        timeSinceLastCall < 0
      );
    }
    
    // 立即執行函式
    function leadingEdge(time) {
      // 1、開啟定時器,為了事件結束後的那次回撥
      timerId = startTimer(timerExpired, wait);
      // 1、如果配置了 leading 執行傳入函式 func
      // leading 來源自 !!options.leading
      return leading ? invokeFunc(time) : result;
    }
    
    const debounced = function (...args) {
      const time = Date.now(); // 獲取當前時間
      const isInvoking = shouldInvoke(time); // 判斷此時是否立即執行 func 函式
    
      lastArgs = args;
      lastThis = this;
      lastCallTime = time;
    
      if (isInvoking) {
        // 判斷是否立即執行
        if (timerId === undefined) {
          return leadingEdge(lastCallTime);
        }
      }
      if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait);
      }
      return result;
    };
    
    debounced.cancel = cancel;
    return debounced;
}

    上述程式碼:
        1. 增加trailingEdge、trailingEdge以及invokeFunc函式
        2. options目前只支援傳入leading引數,也就是immediate。
  • 再往後,我們將options中的trailing加上,也就是這四部分
    • func (Function): 要防抖動的函式。
    • [wait=0] (number): 需要延遲的毫秒數。
    • [options=] (Object): 選項物件。
    • [options.leading=false] (boolean): 指定在延遲開始前呼叫。
    • [options.trailing=true] (boolean): 指定在延遲結束後呼叫。
function debounce(func, wait, options) {
    let timerId, // setTimeout 生成的定時器控制程式碼
      lastThis, // 儲存上一次 this
      lastArgs, // 儲存上一次執行 debounced 的 arguments
      result, // 函式 func 執行後的返回值,多次觸發但未滿足執行 func 條件時,返回 result
      lastCallTime; // 上一次呼叫 debounce 的時間
    
    let leading = false; // 判斷是否立即執行,預設false
    let trailing = true; // 是否響應事件結束後的那次回撥,即最後一次觸發,false 時忽略,預設為true
    
    wait = +wait || 0;
    
    // 從options中獲取是否立即執行
    if (isObject(options)) {
      leading = !!options.leading;
      trailing = "trailing" in options ? !!options.trailing : trailing;
    }
    // 沒傳 wait 時呼叫 window.requestAnimationFrame()
    const useRAF =
      !wait &&
      wait !== 0 &&
      typeof window.requestAnimationFrame === "function";
    
    // 取消debounce
    function cancel() {
      if (timerId !== undefined) {
        cancelTimer(timerId);
      }
      lastArgs = lastThis = timerId = result = lastCallTime = undefined;
    }
    
    // 開啟定時器
    function startTimer(pendingFunc, wait) {
      if (useRAF) {
        window.cancelAnimationFrame(timerId);
        return window.requestAnimationFrame(pendingFunc);
      }
      return setTimeout(pendingFunc, wait);
    }
    
    // 定時器回撥函式,表示定時結束後的操作
    function timerExpired(wait) {
      const time = Date.now();
      // 1、是否需要執行
      // 執行事件結束後的那次回撥,否則重啟定時器
      if (shouldInvoke(time)) {
        return trailingEdge(time);
      }
      // 2、否則 計算剩餘等待時間,重啟定時器,保證下一次時延的末尾觸發
      timerId = startTimer(timerExpired, remainingWait(time));
    }
    
    function remainingWait(time) {
      const timeSinceLastCall = time - lastCallTime;
      const timeWaiting = wait - timeSinceLastCall;
    
      return timeWaiting;
    }
    
    // 這裡時觸發後仍呼叫函式
    function trailingEdge(time) {
      timerId = undefined;
    
      // 這意味著`func'已經被呼叫過一次。
      if (trailing && lastArgs) {
        return invokeFunc(time);
      }
      lastArgs = lastThis = undefined;
      return result;
    }
    
    // 取消定時器
    function cancelTimer(id) {
      if (useRAF) {
        return window.cancelAnimationFrame(id);
      }
      clearTimeout(id);
    }
    
    function invokeFunc(time) {
      const args = lastArgs;
      const thisArg = lastThis;
      lastArgs = lastThis = undefined; // 清空當前函式指向的this,argumnents
      result = func.apply(thisArg, args); // 繫結當前函式指向的this,argumnents
      return result;
    }
    // 判斷此時是否立即執行 func 函式
    // lastCallTime === undefined 第一次呼叫時
    // timeSinceLastCall >= wait 超過超時時間 wait,處理事件結束後的那次回撥
    // timeSinceLastCall < 0 當前時間 - 上次呼叫時間小於 0,即更改了系統時間
    function shouldInvoke(time) {
      const timeSinceLastCall = time - lastCallTime;
      return (
        lastCallTime === undefined ||
        timeSinceLastCall >= wait ||
        timeSinceLastCall < 0
      );
    }
    
    // 立即執行函式
    function leadingEdge(time) {
      // 1、開啟定時器,為了事件結束後的那次回撥
      timerId = startTimer(timerExpired, wait);
      // 1、如果配置了 leading 執行傳入函式 func
      // leading 來源自 !!options.leading
      return leading ? invokeFunc(time) : result;
    }
    
    const debounced = function (...args) {
      const time = Date.now(); // 獲取當前時間
      const isInvoking = shouldInvoke(time); // 判斷此時是否立即執行 func 函式
    
      lastArgs = args;
      lastThis = this;
      lastCallTime = time;
    
      if (isInvoking) {
        // 判斷是否立即執行
        if (timerId === undefined) {
          return leadingEdge(lastCallTime);
        }
      }
      if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait);
      }
      return result;
    };
    
    debounced.cancel = cancel;
    return debounced;
}
    上述程式碼:
        1.leading和trailing不能同時為false。

其實可以在程式碼中加上判斷同時為false時,預設wait=0,直接執行window.requestAnimationFrame部分,而不是定時器。

  • 最後結合maxWait,也就是將防抖和節流合併的關鍵。
    • func (Function): 要防抖動的函式。
    • [wait=0] (number): 需要延遲的毫秒數。
    • [options=] (Object): 選項物件。
    • [options.leading=false] (boolean): 指定在延遲開始前呼叫。
    • [options.maxWait] (number): 設定 func 允許被延遲的最大值。
    • [options.trailing=true] (boolean): 指定在延遲結束後呼叫。

首先,我們可以先來看lodash throttle部分原始碼:

import debounce from './debounce.js'
import isObject from './isObject.js
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

其實就是將wait傳入了debounce函式的option.maxWait中。所以最後,我們只需要將之前的程式碼加上maxWait引數部分。

function debounce(func, wait, options) {
    let timerId, // setTimeout 生成的定時器控制程式碼
      lastThis, // 儲存上一次 this
      lastArgs, // 儲存上一次執行 debounced 的 arguments
      result, // 函式 func 執行後的返回值,多次觸發但未滿足執行 func 條件時,返回 result
      lastCallTime,
      maxWait; // 上一次呼叫 debounce 的時間
    
    let leading = false; // 判斷是否立即執行,預設false
    let trailing = true; // 是否響應事件結束後的那次回撥,即最後一次觸發,false 時忽略,預設為true
    
    /**
     * 節流部分引數
     **/
    let lastInvokeTime = 0; // 上一次執行 func 的時間,配合 maxWait 多用於節流相關
    let maxing = false; // 是否有最大等待時間,配合 maxWait 多用於節流相關
    
    wait = +wait || 0;
    
    // 從options中獲取是否立即執行
    if (isObject(options)) {
      leading = !!options.leading;
      trailing = "trailing" in options ? !!options.trailing : trailing;
    
      /**
       * 節流部分引數
       **/
      maxing = "maxWait" in options; // options 中是否有 maxWait 屬性,節流函式預留
      maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; // maxWait 為設定的 maxWait 和 wait 中最大的
      // 如果 maxWait < wait,那 maxWait 就沒有意義了
    }
    
    // 沒傳 wait 時呼叫 window.requestAnimationFrame()
    const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === "function";
    
    // 取消debounce
    function cancel() {
      if (timerId !== undefined) {
        cancelTimer(timerId);
      }
      lastInvokeTime = 0;
      leading = false;
      maxing = false;
      trailing = true;
      lastArgs = lastThis = timerId = result = lastCallTime = maxWait = undefined;
    }
    
    // 開啟定時器
    function startTimer(pendingFunc, wait) {
      if (useRAF) {
        window.cancelAnimationFrame(timerId);
        return window.requestAnimationFrame(pendingFunc);
      }
      return setTimeout(pendingFunc, wait);
    }
    
    // 定時器回撥函式,表示定時結束後的操作
    function timerExpired(wait) {
      const time = Date.now();
      // 1、是否需要執行
      // 執行事件結束後的那次回撥,否則重啟定時器
      if (shouldInvoke(time)) {
        return trailingEdge(time);
      }
      // 2、否則 計算剩餘等待時間,重啟定時器,保證下一次時延的末尾觸發
      timerId = startTimer(timerExpired, remainingWait(time));
    }
    
    // 計算仍需等待的時間
    function remainingWait(time) {
      // 當前時間距離上一次呼叫 debounce 的時間差
      const timeSinceLastCall = time - lastCallTime;
      // 當前時間距離上一次執行 func 的時間差
      const timeSinceLastInvoke = time - lastInvokeTime;
      // 剩餘等待時間
      const timeWaiting = wait - timeSinceLastCall;
    
      // 是否設定了最大等待時間
      // 是(節流):返回「剩餘等待時間」和「距上次執行 func 的剩餘等待時間」中的最小值
      // 否:返回剩餘等待時間
      return maxing
        ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
        : timeWaiting;
    }
    
    // 這裡時觸發後仍呼叫函式
    function trailingEdge(time) {
      timerId = undefined;
    
      // 這意味著`func'已經被呼叫過一次。
      if (trailing && lastArgs) {
        return invokeFunc(time);
      }
      lastArgs = lastThis = undefined;
      return result;
    }
    
    // 取消定時器
    function cancelTimer(id) {
      if (useRAF) {
        return window.cancelAnimationFrame(id);
      }
      clearTimeout(id);
    }
    
    function invokeFunc(time) {
      const args = lastArgs;
      const thisArg = lastThis;
      lastArgs = lastThis = undefined; // 清空當前函式指向的this,argumnents
    
      lastInvokeTime = time;
      result = func.apply(thisArg, args); // 繫結當前函式指向的this,argumnents
      return result;
    }
    // 判斷此時是否立即執行 func 函式
    // lastCallTime === undefined 第一次呼叫時
    // timeSinceLastCall >= wait 超過超時時間 wait,處理事件結束後的那次回撥
    // timeSinceLastCall < 0 當前時間 - 上次呼叫時間小於 0,即更改了系統時間
    // maxing && timeSinceLastInvoke >= maxWait 超過最大等待時間
    function shouldInvoke(time) {
      // 當前時間距離上一次呼叫 debounce 的時間差
      const timeSinceLastCall = time - lastCallTime;
      // 當前時間距離上一次執行 func 的時間差
      const timeSinceLastInvoke = time - lastInvokeTime;
    
      // 上述 4 種情況返回 true
      return (
        lastCallTime === undefined ||
        timeSinceLastCall >= wait ||
        timeSinceLastCall < 0 ||
        (maxing && timeSinceLastInvoke >= maxWait)
      );
    }
    
    // 立即執行函式
    function leadingEdge(time) {
      // 1、設定上一次執行 func 的時間
      lastInvokeTime = time;
      // 2、開啟定時器,為了事件結束後的那次回撥
      timerId = startTimer(timerExpired, wait);
      // 3、如果配置了 leading 執行傳入函式 func
      // leading 來源自 !!options.leading
      return leading ? invokeFunc(time) : result;
    }
    
    const debounced = function (...args) {
      const time = Date.now(); // 獲取當前時間
      const isInvoking = shouldInvoke(time); // 判斷此時是否立即執行 func 函式
    
      lastArgs = args;
      lastThis = this;
      lastCallTime = time;
    
      if (isInvoking) {
        // 判斷是否立即執行
        if (timerId === undefined) {
          return leadingEdge(lastCallTime);
        }
        // 如果設定了最大等待時間,則立即執行 func
        // 1、開啟定時器,到時間後觸發 trailingEdge 這個函式。
        // 2、執行 func,並返回結果
        if (maxing) {
          // 迴圈定時器中處理呼叫
          timerId = startTimer(timerExpired, wait);
          return invokeFunc(lastCallTime);
        }
      }
      if (timerId === undefined) {
        timerId = startTimer(timerExpired, wait);
      }
      return result;
    };
    
    debounced.cancel = cancel;
    return debounced;
}
上述程式碼:
    儘管程式碼有點長,但是實際上只是增加了maxWait。

下面我們分析下maxWait新增的那部分程式碼。

分析maxWait新增部分

// 1.定義變數
let maxWait; // 上一次呼叫 debounce 的時間
let lastInvokeTime = 0; // 上一次執行 func 的時間,配合 maxWait 多用於節流相關
let maxing = false; // 是否有最大等待時間,配合 maxWait 多用於節流相關


// 2.從options中取出maxWait
if (isObject(options)) {
  maxing = "maxWait" in options; // options 中是否有 maxWait 屬性,節流函式預留
  maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; // maxWait 為設定的 maxWait 和 wait 中最大的
  // 如果 maxWait < wait,那 maxWait 就沒有意義了
}

// 3.計算仍需等待的時間
function remainingWait(time) {
  // 當前時間距離上一次呼叫 debounce 的時間差
  const timeSinceLastCall = time - lastCallTime;
  // 當前時間距離上一次執行 func 的時間差
  const timeSinceLastInvoke = time - lastInvokeTime;
  // 剩餘等待時間
  const timeWaiting = wait - timeSinceLastCall;

  // 是否設定了最大等待時間
  // 是(節流):返回「剩餘等待時間」和「距上次執行 func 的剩餘等待時間」中的最小值
  // 否:返回剩餘等待時間
  return maxing
    ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
    : timeWaiting;
}

// 4.判斷是否立即執行
function shouldInvoke(time) {
  // 當前時間距離上一次呼叫 debounce 的時間差
  const timeSinceLastCall = time - lastCallTime;
  // 當前時間距離上一次執行 func 的時間差
  const timeSinceLastInvoke = time - lastInvokeTime;

  // 上述 4 種情況返回 true
  return (
    lastCallTime === undefined ||
    timeSinceLastCall >= wait ||
    timeSinceLastCall < 0 ||
    (maxing && timeSinceLastInvoke >= maxWait)
  );
}

// 5.有maxing時,應該如何處理函式
if (isInvoking) {
    // 判斷是否立即執行
    if (timerId === undefined) {
      return leadingEdge(lastCallTime);
    }
    // 如果設定了最大等待時間,則立即執行 func
    // 1、開啟定時器,到時間後觸發 trailingEdge 這個函式。
    // 2、執行 func,並返回結果
    if (maxing) {
      // 迴圈定時器中處理呼叫
      timerId = startTimer(timerExpired, wait);
      return invokeFunc(lastCallTime);
    }
}
 

1.新增變數就不多說了。

2.從options中取出maxWait:

// 2.從options中取出maxWait
if (isObject(options)) {
  maxing = "maxWait" in options; // options 中是否有 maxWait 屬性,節流函式預留
  maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; // maxWait 為設定的 maxWait 和 wait 中最大的
  // 如果 maxWait < wait,那 maxWait 就沒有意義了
}
  • 1.這裡主要是將maxing,判斷是否傳了maxWait引數。
  • 2.如果未傳則maxWait還是為初始定義的undefined
  • 3.如果傳入了maxWait,則重新賦值Math.max(+options.maxWait || 0, wait)。這裡主要就是取maxWaitwait中的大值。

3.計算仍需等待的時間

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

首先判斷是否節流(maxing):

  1. 是=>取「剩餘等待時間」和「距上次執行 func 的剩餘等待時間」中的最小值。
  2. 否=>取剩餘等待時間
maxWait - (time - lastInvokeTime)

這裡是不是就是節流中

// 下次觸發 func 剩餘時間 
const remaining = wait - (now - previous);

4.判斷是否立即執行
lodash程式碼:

maxing && (time - lastInvokeTime) >= maxWait

就往下執行。

這裡是不是就是節流中

if (remaining <= 0 || remaining > wait) 

就往下執行。

5.有maxing時,應該如何處理函式
lodash程式碼:如果是節流函式就執行

// 迴圈定時器中處理呼叫
timerId = startTimer(timerExpired, wait);
return invokeFunc(lastCallTime);

節流函式中:

timeout = setTimeout(function () {
    timeout = null;
    previous = options.leading === false ? 0 : getNow(); // 這裡是將previous重新賦值當前時間
    showResult(context, args);
}, remaining);

總之,lodashmaxWait部分,儘管引數名多,但實際上就是節流函式中,判斷剩餘時間remaining。不需要等待,就直接立即執行,否則就到剩餘時間就執行一次,依次類推。

對外 3 個方法

debounced.cancel = cancel // 取消函式延遲執行
debounced.flush = flush // 立即執行 func
debounced.pending = pending // 檢查當前是否在計時中

演示地址

可以去Github倉庫檢視演示程式碼

跟著大佬學系列

主要是日常對每個進階知識點的摸透,跟著大佬一起去深入瞭解JavaScript的語言藝術。

後續會一直更新,希望各位看官不要吝嗇手中的贊。

❤️ 感謝各位的支援!!!

❤️ 如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝!!!

❤️ 喜歡或者有所啟發,歡迎 star!!!

參考

原文地址

【跟著大佬學JavaScript】之lodash防抖節流合併

相關文章