更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端
------ 如下是正文 ------git
前面幾節咱們學習了節流函數 throttle,防抖函數 debounce,以及各自如何在 React 項目中進行應用,今天這篇文章主要聊聊 Lodash 中防抖和節流函數是如何實現的,並對源碼淺析一二。下篇文章會舉幾個小例子爲切入點,換種方式繼續解讀源碼,敬請期待。github
有什麼想法或者意見均可以在評論區留言,歡迎你們拍磚。面試
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,第三個是可選參數,分別是 leading
、trailing
和 maxWait
。函數
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
}
複製代碼
入口函數中屢次使用了 startTimer
、timerExpired
這些方法,都是和定時器以及時間計算相關的,除了這兩個方法外還有 cancelTimer
和 remainingWait
。學習
這個就是開啓定時器了,防抖和節流的核心仍是使用定時器,當事件觸發時,設置一個指定超時時間的定時器,並傳入回調函數,此時的回調函數 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)
}
複製代碼
定時器有開啓天然就須要關閉,關閉很簡單,只要區分好 RAF 和非 RAF 時的狀況便可,取消時傳入時間 id。
// 取消定時器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
複製代碼
startTimer
函數中傳入的回調函數 pendingFunc
其實就是定時器回調函數 timerExpired
,表示定時結束後的操做。
定時結束後無非兩種狀況,一種是執行傳入函數 func,另外一種就是不執行。對於第一種須要判斷下是否須要執行傳入函數 func,須要的時候執行最後一次回調。對於第二種計算剩餘等待時間並重啓定時器,保證下一次時延的末尾觸發。
// 定時器回調函數,表示定時結束後的操做
function timerExpired() {
const time = Date.now()
// 一、是否須要執行
// 執行事件結束後的那次回調,不然重啓定時器
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 二、不然 計算剩餘等待時間,重啓定時器,保證下一次時延的末尾觸發
timerId = startTimer(timerExpired, remainingWait(time))
}
複製代碼
這裏計算仍然須要等待的時間,使用的變量有點多,足足有 9 個,咱們先看看各個變量的含義。
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
。
執行事件剛開始的那次回調,即事件剛觸發就執行,再也不等待 wait 時間以後,在這個方法裏主要有三步。
lastInvokeTime
// 執行連續事件剛開始的那次回調
function leadingEdge(time) {
// 一、設置上一次執行 func 的時間
lastInvokeTime = time
// 二、開啓定時器,爲了事件結束後的那次回調
timerId = startTimer(timerExpired, wait)
// 三、若是配置了 leading 執行傳入函數 func
// leading 來源自 !!options.leading
return leading ? invokeFunc(time) : result
}
複製代碼
這裏就是執行事件結束後的回調了,這裏作的事情很簡單,就是執行 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
}
複製代碼
說了那麼屢次執行 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
}
複製代碼
在入口函數中執行 invokeFunc
時會先判斷下是否應該執行,咱們來詳細看下具體邏輯,和 remainingWait
中相似,變量有點多,先來回顧下這些變量。
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))
}
複製代碼
debounced
函數提供了 3 個方法,分別是cancel
、flush
和 pending
,經過以下方式提供屬性進行綁定。
// 綁定方法
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
複製代碼
這個就是取消執行,取消主要作的就是清除定時器,而後清除必要的閉包變量,迴歸初始狀態。
// 取消函數延遲執行
function cancel() {
// 清除定時器
if (timerId !== undefined) {
cancelTimer(timerId)
}
// 清除閉包變量
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
複製代碼
這個是對外提供的當即執行方法,方便須要的時候調用。
result
結果trailingEdge
,執行完成後會清空定時器id,lastArgs
和 lastThis
// 當即執行 func
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
複製代碼
獲取當前狀態,檢查當前是否在計時中,存在定時器 id timerId
意味着正在計時中。
// 檢查當前是否在計時中
function pending() {
return timerId !== undefined
}
複製代碼
節流函數的定義和自定義實現我就再也不介紹了,以前專門寫過一篇文章,戳這裏學習
這部分源碼比較簡單,相比防抖來講只是觸發條件不一樣,說白了就是 maxWait
爲 wait
的防抖函數。
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
判斷是不是一個對象,原理就是 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
複製代碼
源碼解析已經完成,那你真的理解了嗎,留下幾道思考題給你們,歡迎做答,答案會在下篇文章中給出。
leading
和 trailing
選項都是 true,在 wait
期間只調用了一次 debounced
函數時,總共會調用幾回 func
,1 次仍是 2 次,爲何?debounce(func, time, options)
中的 func
傳參數?若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙: