【進階 7-4 期】深刻篇 | 阿里 P6 必會 Lodash 防抖節流函數實現原理

更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端

------ 如下是正文 ------git

引言

前面幾節咱們學習了節流函數 throttle,防抖函數 debounce,以及各自如何在 React 項目中進行應用,今天這篇文章主要聊聊 Lodash 中防抖和節流函數是如何實現的,並對源碼淺析一二。下篇文章會舉幾個小例子爲切入點,換種方式繼續解讀源碼,敬請期待。github

有什麼想法或者意見均可以在評論區留言,歡迎你們拍磚。面試

防抖函數 debounce

Lodash 中節流函數比較簡單,直接調用防抖函數,傳入一些配置就搖身一變成了節流函數,因此咱們先來看看其中防抖函數是如何實現的,弄懂了防抖,那節流天然就容易理解了。瀏覽器

防抖函數的定義和自定義實現我就再也不介紹了,以前專門寫過一篇文章,戳這裏學習閉包

進入正文,咱們看下 debounce 源碼,源碼很少,總共 100 多行,爲了方便理解就先列出代碼結構,而後再從入口函數着手一個一個的介紹。app

代碼結構

function debounce(func, wait, options) {
  // 經過閉包保存一些變量
  let lastArgs, // 上一次執行 debounced 的 arguments,
      					// 起一個標記位的做用,用於 trailingEdge 方法中,invokeFunc 後清空
    lastThis, // 保存上一次 this
    maxWait, // 最大等待時間,數據來源於 options,實現節流效果,保證大於必定時間後必定能執行
    result, // 函數 func 執行後的返回值,屢次觸發但未知足執行 func 條件時,返回 result
    timerId, // setTimeout 生成的定時器句柄
    lastCallTime // 上一次調用 debounce 的時間

  let lastInvokeTime = 0 // 上一次執行 func 的時間,配合 maxWait 多用於節流相關
  let leading = false // 是否響應事件剛開始的那次回調,即第一次觸發,false 時忽略
  let maxing = false // 是否有最大等待時間,配合 maxWait 多用於節流相關
  let trailing = true // 是否響應事件結束後的那次回調,即最後一次觸發,false 時忽略

  // 沒傳 wait 時調用 window.requestAnimationFrame()
  // window.requestAnimationFrame() 告訴瀏覽器但願執行動畫並請求瀏覽器在下一次重繪以前調用指定的函數來更新動畫,差很少 16ms 執行一次
  const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')

  // 保證輸入的 func 是函數,不然報錯
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  
  // 轉成 Number 類型
  wait = +wait || 0
  
  // 獲取用戶傳入的配置 options
  if (isObject(options)) {
    leading = !!options.leading
    // options 中是否有 maxWait 屬性,節流函數預留
    maxing = 'maxWait' in options
    // maxWait 爲設置的 maxWait 和 wait 中最大的,若是 maxWait 小於 wait,那 maxWait 就沒有意義了
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }
  
  // ----------- 開閉定時器 -----------
  // 開啓定時器
  function startTimer(pendingFunc, wait) {}

  // 取消定時器
  function cancelTimer(id) {}

  // 定時器回調函數,表示定時結束後的操做
  function timerExpired() {}
  
  // 計算仍需等待的時間
  function remainingWait(time) {}
  
  // ----------- 執行傳入函數 -----------
	// 執行連續事件剛開始的那次回調
  function leadingEdge(time) {}
  
  // 執行連續事件結束後的那次回調
  function trailingEdge(time) {}

  // 執行 func 函數
  function invokeFunc(time) {}

  // 判斷此時是否應該執行 func 函數
  function shouldInvoke(time) {}

  // ----------- 對外 3 個方法 -----------
  // 取消函數延遲執行
  function cancel() {}

  // 當即執行 func
  function flush() {}

  // 檢查當前是否在計時中
  function pending() {}

  // ----------- 入口函數 -----------
  function debounced(...args) {}
  
  // 綁定方法
  debounced.cancel = cancel
  debounced.flush = flush
  debounced.pending = pending
  
  // 返回入口函數 
  return debounced
}
複製代碼

debounce(func, wait, options) 方法提供了 3 個參數,第一個是咱們想要執行的函數,爲方便理解文中統一稱爲傳入函數 func,第二個是超時時間 wait,第三個是可選參數,分別是 leadingtrailingmaxWait函數

入口函數

debounce 函數最終返回了 debounced,返回的這個函數就是入口函數了,事件每次觸發後都會執行 debounced 函數,並且會頻繁的執行,因此在這個方法裏須要「判斷是否應該執行傳入函數 func」,而後根據條件開啓定時器,debounced 函數作的就是這件事。post

// 入口函數,返回此函數
function debounced(...args) {
  // 獲取當前時間
  const time = Date.now()
  // 判斷此時是否應該執行 func 函數
  const isInvoking = shouldInvoke(time)

  // 賦值給閉包,用於其餘函數調用
  lastArgs = args
  lastThis = this
  lastCallTime = time

  // 執行
  if (isInvoking) {
    // 無 timerId 的狀況有兩種:
    // 一、首次調用 
    // 二、trailingEdge 執行過函數
    if (timerId === undefined) {
      return leadingEdge(lastCallTime)
    }
    
    // 若是設置了最大等待時間,則當即執行 func
    // 一、開啓定時器,到時間後觸發 trailingEdge 這個函數。
    // 二、執行 func,並返回結果
    if (maxing) {
      // 循環定時器中處理調用
      timerId = startTimer(timerExpired, wait)
      return invokeFunc(lastCallTime)
    }
  }
  // 一種特殊狀況,trailing 設置爲 true 時,前一個 wait 的 trailingEdge 已經執行了函數
  // 此時函數被調用時 shouldInvoke 返回 false,因此要開啓定時器
  if (timerId === undefined) {
    timerId = startTimer(timerExpired, wait)
  }
  // 不須要執行時,返回結果
  return result
}
複製代碼

開閉定時器

入口函數中屢次使用了 startTimertimerExpired 這些方法,都是和定時器以及時間計算相關的,除了這兩個方法外還有 cancelTimerremainingWait學習

startTimer

這個就是開啓定時器了,防抖和節流的核心仍是使用定時器,當事件觸發時,設置一個指定超時時間的定時器,並傳入回調函數,此時的回調函數 pendingFunc 其實就是 timerExpired。這裏區分兩種狀況,一種是使用 requestAnimationFrame,另外一種是使用 setTimeout

// 開啓定時器
function startTimer(pendingFunc, wait) {
  // 沒傳 wait 時調用 window.requestAnimationFrame()
  if (useRAF) {
    // 若想在瀏覽器下次重繪以前繼續更新下一幀動畫
    // 那麼回調函數自身必須再次調用 window.requestAnimationFrame()
    root.cancelAnimationFrame(timerId);
    return root.requestAnimationFrame(pendingFunc)
  }
  // 不使用 RAF 時開啓定時器
  return setTimeout(pendingFunc, wait)
}
複製代碼

cancelTimer

定時器有開啓天然就須要關閉,關閉很簡單,只要區分好 RAF 和非 RAF 時的狀況便可,取消時傳入時間 id。

// 取消定時器
function cancelTimer(id) {
  if (useRAF) {
    return root.cancelAnimationFrame(id)
  }
  clearTimeout(id)
}
複製代碼

timerExpired

startTimer 函數中傳入的回調函數 pendingFunc 其實就是定時器回調函數 timerExpired,表示定時結束後的操做。

定時結束後無非兩種狀況,一種是執行傳入函數 func,另外一種就是不執行。對於第一種須要判斷下是否須要執行傳入函數 func,須要的時候執行最後一次回調。對於第二種計算剩餘等待時間並重啓定時器,保證下一次時延的末尾觸發。

// 定時器回調函數,表示定時結束後的操做
function timerExpired() {
  const time = Date.now()
  // 一、是否須要執行
  // 執行事件結束後的那次回調,不然重啓定時器
  if (shouldInvoke(time)) {
    return trailingEdge(time)
  }
  // 二、不然 計算剩餘等待時間,重啓定時器,保證下一次時延的末尾觸發
  timerId = startTimer(timerExpired, remainingWait(time))
}
複製代碼

remainingWait

這裏計算仍然須要等待的時間,使用的變量有點多,足足有 9 個,咱們先看看各個變量的含義。

  • time 當前時間戳
  • lastCallTime 上一次調用 debounce 的時間
  • timeSinceLastCall 當前時間距離上一次調用 debounce 的時間差
  • lastInvokeTime 上一次執行 func 的時間
  • timeSinceLastInvoke 當前時間距離上一次執行 func 的時間差
  • wait 輸入的等待時間
  • timeWaiting 剩餘等待時間
  • maxWait 最大等待時間,數據來源於 options,爲了節流函數預留
  • maxing 是否設置了最大等待時間,判斷依據是 maxWait in options
  • maxWait - timeSinceLastInvoke 距上次執行 func 的剩餘等待時間

變量是真的多,沒看明白建議再看一遍,固然核心是下面這部分,根據 maxing 判斷具體應該返回的剩餘等待時間。

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

這部分比較核心,完整的代碼註釋以下。

// 計算仍需等待的時間
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
}
複製代碼

執行傳入函數

聊完定時器和時間相關的函數後,這部分源碼解析已經進行了大半,接下來咱們看一下執行傳入函數 func 的邏輯,分爲執行剛開始的那次回調 leadingEdge,執行結束後的那次回調 trailingEdge,正常執行 func 函數 invokeFunc,以及判斷是否應該執行 func 函數 shouldInvoke

leadingEdge

執行事件剛開始的那次回調,即事件剛觸發就執行,再也不等待 wait 時間以後,在這個方法裏主要有三步。

  • 設置上一次執行 func 的時間 lastInvokeTime
  • 開啓定時器
  • 執行傳入函數 func
// 執行連續事件剛開始的那次回調
function leadingEdge(time) {
  // 一、設置上一次執行 func 的時間
  lastInvokeTime = time
  // 二、開啓定時器,爲了事件結束後的那次回調
  timerId = startTimer(timerExpired, wait)
  // 三、若是配置了 leading 執行傳入函數 func
  // leading 來源自 !!options.leading
  return leading ? invokeFunc(time) : result
}
複製代碼

trailingEdge

這裏就是執行事件結束後的回調了,這裏作的事情很簡單,就是執行 func 函數,以及清空參數。

// 執行連續事件結束後的那次回調
function trailingEdge(time) {
  // 清空定時器
  timerId = undefined

  // trailing 和 lastArgs 二者同時存在時執行
  // trailing 來源自 'trailing' in options ? !!options.trailing : trailing
  // lastArgs 標記位的做用,意味着 debounce 至少執行過一次
  if (trailing && lastArgs) {
    return invokeFunc(time)
  }
  // 清空參數
  lastArgs = lastThis = undefined
  return result
}
複製代碼

invokeFunc

說了那麼屢次執行 func 函數,那麼具體是如何執行的呢?真的很簡單,就是 func.apply(thisArg, args),除此以外須要重置部分參數。

// 執行 Func 函數
function invokeFunc(time) {
  // 獲取上一次執行 debounced 的參數
  const args = lastArgs
  // 獲取上一次的 this
  const thisArg = lastThis

  // 重置
  lastArgs = lastThis = undefined
  lastInvokeTime = time
  result = func.apply(thisArg, args)
  return result
}
複製代碼

shouldInvoke

在入口函數中執行 invokeFunc 時會先判斷下是否應該執行,咱們來詳細看下具體邏輯,和 remainingWait 中相似,變量有點多,先來回顧下這些變量。

  • time 當前時間戳
  • lastCallTime 上一次調用 debounce 的時間
  • timeSinceLastCall 當前時間距離上一次調用 debounce 的時間差
  • lastInvokeTime 上一次執行 func 的時間
  • timeSinceLastInvoke 當前時間距離上一次執行 func 的時間差
  • wait 輸入的等待時間
  • maxWait 最大等待時間,數據來源於 options,爲了節流函數預留
  • maxing 是否設置了最大等待時間,判斷依據是 maxWait in options

咱們來一步一步看下判斷的核心代碼,總共有 4 種邏輯。

return ( lastCallTime === undefined || 
       (timeSinceLastCall >= wait) ||
       (timeSinceLastCall < 0) || 
       (maxing && timeSinceLastInvoke >= maxWait) )
複製代碼

會發現一共有 4 種狀況返回 true,區分開看也比較理解。

  • lastCallTime === undefined 第一次調用時
  • timeSinceLastCall >= wait 超過超時時間 wait,處理事件結束後的那次回調
  • timeSinceLastCall < 0 當前時間 - 上次調用時間小於 0,即更改了系統時間
  • maxing && timeSinceLastInvoke >= maxWait 超過最大等待時間
// 判斷此時是否應該執行 func 函數
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))
}
複製代碼

對外 3 個方法

debounced 函數提供了 3 個方法,分別是cancelflushpending,經過以下方式提供屬性進行綁定。

// 綁定方法
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
複製代碼

cancel

這個就是取消執行,取消主要作的就是清除定時器,而後清除必要的閉包變量,迴歸初始狀態。

// 取消函數延遲執行
function cancel() {
  // 清除定時器
  if (timerId !== undefined) {
    cancelTimer(timerId)
  }
  // 清除閉包變量
  lastInvokeTime = 0
  lastArgs = lastCallTime = lastThis = timerId = undefined
}
複製代碼

flush

這個是對外提供的當即執行方法,方便須要的時候調用。

  • 若是不存在定時器,意味着尚未觸發事件或者事件已經執行完成,則此時返回 result 結果
  • 若是存在定時器,當即執行 trailingEdge,執行完成後會清空定時器id,lastArgslastThis
// 當即執行 func
function flush() {
  return timerId === undefined ? result : trailingEdge(Date.now())
}
複製代碼

pending

獲取當前狀態,檢查當前是否在計時中,存在定時器 id timerId 意味着正在計時中。

// 檢查當前是否在計時中
function pending() {
  return timerId !== undefined
}
複製代碼

節流函數 throttle

節流函數的定義和自定義實現我就再也不介紹了,以前專門寫過一篇文章,戳這裏學習

throttle

這部分源碼比較簡單,相比防抖來講只是觸發條件不一樣,說白了就是 maxWaitwait 的防抖函數。

function throttle(func, wait, options) {
  // 首尾調用默認爲 true
  let leading = true
  let trailing = true

  if (typeof func !== 'function') {
    throw new TypeError('Expected a function')
  }
  // options 是不是對象
  if (isObject(options)) {
    leading = 'leading' in options ? !!options.leading : leading
    trailing = 'trailing' in options ? !!options.trailing : trailing
  }
  // maxWait 爲 wait 的防抖函數
  return debounce(func, wait, {
    leading,
    trailing,
    'maxWait': wait,
  })
}
複製代碼

isObject()

上面使用了 isObject 判斷是不是一個對象,原理就是 typeof value,若是是 object 或者 function 時返回 true。

function isObject(value) {
  const type = typeof value
  return value != null && (type == 'object' || type == 'function')
}
複製代碼

舉幾個小例子說明下

isObject({})
// => true

isObject([1, 2, 3])
// => true

isObject(Function)
// => true

isObject(null)
// => false
複製代碼

思考題

源碼解析已經完成,那你真的理解了嗎,留下幾道思考題給你們,歡迎做答,答案會在下篇文章中給出。

  • 若是 leadingtrailing 選項都是 true,在 wait 期間只調用了一次 debounced 函數時,總共會調用幾回 func,1 次仍是 2 次,爲何?
  • 如何給 debounce(func, time, options) 中的 func 傳參數?

參考

從lodash源碼學習節流與防抖

推薦閱讀

【進階 6-3 期】深刻淺出節流函數 throttle

【進階 6-4 期】深刻淺出防抖函數 debounce

【進階 6-5 期】[譯] Throttle 和 Debounce 在 React 中的應用

❤️ 看完三件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-
  2. 關注個人 GitHub,讓咱們成爲長期關係
  3. 關注公衆號「高級前端進階」,每週重點攻克一個前端面試重難點,公衆號後臺回覆「資料」 送你精選前端優質資料。

相關文章
相關標籤/搜索