更新:謝謝你們的支持,最近折騰了一個博客官網出來,方便你們系統閱讀,後續會有更多內容和更多優化,猛戳這裏查看前端
------ 如下是正文 ------git
上一節咱們學習了 Lodash 中防抖和節流函數是如何實現的,並對源碼淺析一二,今天這篇文章會經過七個小例子爲切入點,換種方式繼續解讀源碼。其中源碼解析上篇文章已經很是詳細介紹了,這裏就再也不重複,建議本文配合上文一塊兒服用,猛戳這裏學習github
有什麼想法或者意見均可以在評論區留言,歡迎你們拍磚。面試
咱們先來看一張圖,這張圖充分說明了 Throttle(節流)和 Debounce(防抖)的區別,以及在不一樣配置下產生的不一樣效果,其中 mousemove
事件每 50 ms 觸發一次,即下圖中的每一小隔是 50 ms。今天這篇文章就從下面這張圖開始介紹。segmentfault
lodash.throttle(fn, 200, {leading: true, trailing: true})
瀏覽器
先來看下 throttle 源碼閉包
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,
})
}
複製代碼
因此 throttle(fn, 200, {leading: true, trailing: true})
返回內容是 debounce(fn, 200, {leading: true, trailing: true, maxWait: 200})
,多了 maxWait: 200
這部分。app
先打個預防針,後面即將開始比較難的部分,看下 debounce 入口函數。函數
// 入口函數,返回此函數
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
}
複製代碼
對於 debounce(fn, 200, {leading: true, trailing: true, maxWait: 200})
來講,會經歷以下過程。學習
shouldInvoke(time)
中,由於知足條件 lastCallTime === undefined
,因此返回 truelastCallTime = time
,因此 lastCallTime
等於當前時間,假設爲 0timerId === undefined
知足,執行 leadingEdge(lastCallTime)
方法// 執行連續事件剛開始的那次回調
function leadingEdge(time) {
// 一、設置上一次執行 func 的時間
lastInvokeTime = time
// 二、開啓定時器,爲了事件結束後的那次回調
timerId = startTimer(timerExpired, wait)
// 三、若是配置了 leading 執行傳入函數 func
// leading 來源自 !!options.leading
return leading ? invokeFunc(time) : result
}
複製代碼
leadingEdge(time)
中,設置 lastInvokeTime
爲當前時間即 0,開啓 200 毫秒定時器,執行 invokeFunc(time)
並返回// 執行 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(time)
中,執行 func.apply(thisArg, args)
,即 fn 函數第一次執行,並把結果賦值給 result
,便於後續觸發時直接返回。同時重置 lastInvokeTime
爲當前時間即 0,清空 lastArgs
和 lastThis
。lastCallTime
和 lastInvokeTime
都爲 0,200 毫秒的定時器還在運行中。50 毫秒後第二次觸發到來,此時當前時間 time
爲 50,wait
爲 200, maxWait
爲 200,maxing
爲 true,lastCallTime
和 lastInvokeTime
都爲 0,timerId
定時器存在,咱們來看下執行步驟。
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) )
}
複製代碼
shouldInvoke(time)
中,timeSinceLastCall
爲 50,timeSinceLastInvoke
爲 50,4 種條件都不知足,返回 false。isInvoking
爲 false,同時 timerId === undefined
不知足,直接返回第一次觸發時的 result
result
距第一次觸發 200 毫秒後第五次觸發到來,此時當前時間 time
爲 200,wait
爲 200, maxWait
爲 200,maxing
爲 true,lastCallTime
爲 150, lastInvokeTime
爲 0,timerId
定時器存在,咱們來看下執行步驟。
shouldInvoke(time)
中,timeSinceLastInvoke
爲 200,知足(maxing && timeSinceLastInvoke >= maxWait)
,因此返回 true// debounced 方法中執行到這部分
if (maxing) {
// 循環定時器中處理調用
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
複製代碼
maxing
條件,從新開啓 200 毫秒的定時器,並執行 invokeFunc(lastCallTime)
函數invokeFunc(time)
中,重置 lastInvokeTime
爲當前時間即 200,清空 lastArgs
和 lastThis
假設第八次觸發以後就中止了滾動,在第八次觸發時 time
爲 350,因此若是有第九次觸發,那麼此時是應該執行fn 的,可是此時 mousemove 已經中止了觸發,那麼還會執行 fn 嗎?答案是依舊執行,由於最開始設置了 {trailing: true}
。
// 開啓定時器
function startTimer(pendingFunc, wait) {
// 沒傳 wait 時調用 window.requestAnimationFrame()
if (useRAF) {
// 若想在瀏覽器下次重繪以前繼續更新下一幀動畫
// 那麼回調函數自身必須再次調用 window.requestAnimationFrame()
root.cancelAnimationFrame(timerId);
return root.requestAnimationFrame(pendingFunc)
}
// 不使用 RAF 時開啓定時器
return setTimeout(pendingFunc, wait)
}
複製代碼
在第五次觸發時開啓了 200 毫秒的定時器,因此在時間 time
到 400 時會執行 pendingFunc
,此時的 pendingFunc
就是 timerExpired
函數,來看下具體的代碼。
// 定時器回調函數,表示定時結束後的操做
function timerExpired() {
const time = Date.now()
// 一、是否須要執行
// 執行事件結束後的那次回調,不然重啓定時器
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 二、不然 計算剩餘等待時間,重啓定時器,保證下一次時延的末尾觸發
timerId = startTimer(timerExpired, remainingWait(time))
}
複製代碼
此時在 shouldInvoke(time)
中,time
爲 400,lastInvokeTime
爲 200,timeSinceLastInvoke
爲 200,知足 (maxing && timeSinceLastInvoke >= maxWait)
,因此返回 true。
// 執行連續事件結束後的那次回調
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
}
複製代碼
以後執行 trailingEdge(time)
,在這個函數中判斷 trailing
和 lastArgs
,此時這兩個條件都是 true,因此會執行 invokeFunc(time)
,最終執行函數 fn。
這裏須要說明如下兩點
{trailing: false}
,那麼最後一次是不會執行的。對於 throttle
和 debounce
來講,默認值是 true,因此若是沒有特地指定 trailing
,那麼最後一次是必定會執行的。lastArgs
來講,執行 debounced
時會賦值,即每次觸發都會從新賦值一次,那何時清空呢,在 invokeFunc(time)
中執行 fn 函數時重置爲 undefined
,因此若是 debounced
只觸發了一次,即便設置了 {trailing: true}
那也不會再執行 fn 函數,這個就解答了上篇文章留下的第一道思考題。lodash.throttle(fn, 200, {leading: true, trailing: false})
在「角度 1 之 mousemove 中止觸發」這部分中說到,若是不設置 trailing
和設置 {trailing: true}
效果是同樣的,事件回調結束後都會再執行一次傳入函數 fn,可是若是設置了{trailing: false}
,那麼事件回調結束後是不會再執行 fn 的。
此時的配置對比角度 1 來講,區別在於設置了{trailing: false}
,因此實際效果對比 1 來講,就是最後不會額外再執行一次,效果見第一張圖。
lodash.throttle(fn, 200, {leading: false, trailing: true})
此時的配置和角度 1 相比,區別在於設置了 {leading: false}
,因此直接看 leadingEdge(time)
方法就能夠了。
// 執行連續事件剛開始的那次回調
function leadingEdge(time) {
// 一、設置上一次執行 func 的時間
lastInvokeTime = time
// 二、開啓定時器,爲了事件結束後的那次回調
timerId = startTimer(timerExpired, wait)
// 三、若是配置了 leading 執行傳入函數 func
// leading 來源自 !!options.leading
return leading ? invokeFunc(time) : result
}
複製代碼
在這裏,會開啓 200 毫秒的定時器,同時由於 leading
爲 false,因此並不會執行 invokeFunc(time)
,只會返回 result
,此時的 result
值是 undefined
。
這裏開啓一個定時器的目的是爲了事件結束後的那次回調,即若是設置了 {trailing: true}
那麼最後一次回調將執行傳入函數 fn,哪怕 debounced
函數只觸發一次。
這裏指定了 {leading: false}
,那麼 leading
的初始值是什麼呢?在 debounce
中是 false,在 throttle
中是 true。因此在 throttle
中不須要剛開始就觸發時,必須指定 {leading: false}
,在 debounce
中就不須要了,默認不觸發。
lodash.debounce(fn, 200, {leading: false, trailing: true})
此時相比較 throttle 來講,缺乏了 maxWait
值,因此具體觸發過程當中的判斷就不同了,來詳細看一遍。
debounced
中,執行 shouldInvoke(time)
,前面討論過由於第一次觸發因此會返回 true,以後執行 leadingEdge(lastCallTime)
。// 執行連續事件剛開始的那次回調
function leadingEdge(time) {
// 一、設置上一次執行 func 的時間
lastInvokeTime = time
// 二、開啓定時器,爲了事件結束後的那次回調
timerId = startTimer(timerExpired, wait)
// 三、若是配置了 leading 執行傳入函數 func
// leading 來源自 !!options.leading
return leading ? invokeFunc(time) : result
}
複製代碼
leadingEdge
中,由於 leading
爲 false,因此並不執行 fn,只開啓 200 毫秒的定時器,並返回 undefined
。此時 lastInvokeTime
爲當前時間,假設爲 0。// 判斷此時是否應該執行 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) )
}
複製代碼
timeSinceLastCall
老是爲 50 毫秒,maxing
爲 false,因此 shouldInvoke(time)
老是返回 false,並不會執行傳入函數 fn,只返回 result,即爲 undefined
。timerExpired
函數// 定時器回調函數,表示定時結束後的操做
function timerExpired() {
const time = Date.now()
// 一、是否須要執行
// 執行事件結束後的那次回調,不然重啓定時器
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 二、不然 計算剩餘等待時間,重啓定時器,保證下一次時延的末尾觸發
timerId = startTimer(timerExpired, remainingWait(time))
}
複製代碼
mousemove
事件一直在觸發,根據前面介紹 shouldInvoke(time)
會返回 false,以後就將計算剩餘等待時間,重啓定時器。時間計算公式爲 wait - (time - lastCallTime)
,即 200 - 50,因此只要 shouldInvoke(time)
返回 false,就每隔 150 毫秒後執行一次 timerExpired()
。mousemove
事件再也不觸發,由於 timerExpired()
在循環執行,因此確定會存在一種狀況知足 timeSinceLastCall >= wait
,即 shouldInvoke(time)
返回 true,終結 timerExpired()
的循環,並執行 trailingEdge(time)
。// 執行連續事件結束後的那次回調
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
}
複製代碼
trailingEdge
中 trailing
和 lastArgs
都是 true,因此會執行 invokeFunc(time)
,即執行傳入函數 fn。lodash.debounce(fn, 200, {leading: true, trailing: false})
此時相比角度 4 來講,差別在於 {leading: true, trailing: false}
,可是 wait
和 maxWait
都和角度 4 一致,因此只存在下面 2 種區別,效果同上面第一張圖所示。
leadingEdge
中會執行傳入函數 fntrailingEdge
中再也不執行傳入函數 fnlodash.debounce(fn, 200, {leading: true, trailing: true})
此時相比角度 4 來講,差別僅僅在於設置了 {leading: true}
,因此只存在一個區別,那就是在 leadingEdge
中會執行傳入函數 fn,固然在 trailingEdge
中依舊執行傳入函數 fn,因此會出如今 mousemove 事件觸發過程當中首尾都會執行的狀況,效果同上面第一張圖所示。
固然一種狀況除外,那就是 mousemove
事件永遠只觸發一次的狀況,關鍵在於 lastArgs
變量。
對於 lastArgs
變量來講,在入口函數 debounced
中賦值,即每次觸發都會從新賦值一次,那何時清空呢,在 invokeFunc(time)
中重置爲 undefined
,因此若是 debounced
只觸發了一次,並且在 {leading: true}
時執行過一次 fn,那麼即便設置了 {trailing: true}
也不會再執行傳入函數 fn。
lodash.debounce(fn, 200, {leading: false, trailing: true, maxWait: 400})
此時 wait
爲 200,maxWait
爲 400,maxing
爲 true,咱們來看下執行過程。
{leading: false}
,因此確定不會執行 fn,此時開啓了一個 200 毫秒的定時器。// 判斷此時是否應該執行 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) )
}
複製代碼
shouldInvoke(time)
函數,只有在第 400 毫秒時,纔會知足 maxing && timeSinceLastInvoke >= maxWait
,返回 true。// 計算仍需等待的時間
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
}
複製代碼
timerExpired
,由於此時 shouldInvoke(time)
返回 false,因此會從新計算剩餘等待時間並重啓計時器,其中 timeWaiting
是 150 毫秒,maxWait - timeSinceLastInvoke
是 200 毫秒,因此計算結果是150 毫秒。timeWaiting
依舊是 150 毫秒,maxWait - timeSinceLastInvoke
是 50 毫秒,因此從新開啓 50 毫秒的定時器,即在第 400 毫秒時觸發。shouldInvoke(time)
中返回 true 的時間也是在第 400 毫秒,爲何要這樣呢?這樣會衝突嗎?首先定時器剩餘時間判斷和 shouldInvoke(time)
判斷中,只要有一處知足執行 fn 條件,就會立馬執行,同時 lastInvokeTime
值也會發生改變,因此另外一處判斷就不會生效了。另外自己定時器是不精準的,因此經過 Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
取最小值的方式來減小偏差。if (timerId === undefined) {timerId = startTimer(timerExpired, wait)}
,避免 trailingEdge
執行後定時器被清空。問:若是 leading
和 trailing
選項都是 true,在 wait
期間只調用了一次 debounced
函數時,總共會調用幾回 func
,1 次仍是 2 次,爲何?
答案是 1 次,爲何?文中已給出詳細解答,詳情請看角度 1 和角度 6。
問:如何給 debounce(func, time, options)
中的 func
傳參數?
第一種方案,由於 debounced
函數能夠接受參數,因此能夠用高階函數的方式傳參,以下
const params = 'muyiy';
const debounced = lodash.debounce(func, 200)(params)
window.addEventListener('mousemove', debounced);
複製代碼
不過這種方式不太友好,params 會將原來的 event 覆蓋掉,此時就拿不到 scroll 或者 mousemove 等事件對象 event 了。
第二種方案,在監聽函數上處理,使用閉包保存傳入參數並返回須要執行的函數便可。
function onMove(param) {
console.log('param:', param); // muyiy
function func(event) {
console.log('param:', param); // muyiy
console.log('event:', event); // event
}
return func;
}
複製代碼
使用時以下
const params = 'muyiy';
const debounced = lodash.debounce(onMove(params), 200)
window.addEventListener('mousemove', debounced);
複製代碼
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙: