聊聊lodash的debounce實現

2021-09-11 13:54:16 字數 3596 閱讀 8621

本文同步自我的blog

前段時間團隊內部搞了乙個**訓練營,大家組織在一起實現lodashthrottledebounce,實現起來覺得並不麻煩,但是最後和官方的一對比,發現功能的實現上還是有差距的,為了尋找我的問題,把官方原始碼閱讀了一遍,本文是我閱讀完成後的一篇總結。

注:本文只會列出比較核心部分的**和注釋,如果對全部的原始碼有興趣的歡迎直接看我的repo:

throttle:將乙個函式的呼叫頻率限制在一定閾值內,例如 1s 內乙個函式不能被呼叫兩次。

debounce:當呼叫函式n秒後,才會執行該動作,若在這n秒內又呼叫該函式則將取消前一次並重新計算執行時間,舉個簡單的例子,我們要根據使用者輸入做suggest,每當使用者按下鍵盤的時候都可以取消前一次,並且只關心最後一次輸入的時間就行了。

lodash對這兩個函式又增加了一些引數,主要是以下三個:

這裡直接劇透一下,其實throttle就是設定了maxwaitdebounce,所以我這裡也只會介紹debounce的**,聰明的讀者們可以自己思考一下為什麼。

我自己的**實現放在我的repo裡,大家有興趣的可以看下。之前說過我的實現和lodash有些區別,下面就用兩張圖來展示一下。

這是我的實現

這是lodash的實現

這裡看到,我的**主要有兩個問題:

throttle的最後一次函式會執行兩次,而且並非穩定復現。

throttle裡函式執行的順序不對,雖然我的功能實現了,但是對於每一次wait來說,我都是執行的leading那一次

下面,我就會帶著這幾個問題去看看lodasah的**。

function debounce(func, wait, options) 

wait = +wait || 0

if (isobject(options))

function invokefunc(time)

function leadingedge(time)

function remainingwait(time)

// 根據時間判斷 func 能否被執行

function shouldinvoke(time)

function

timerexpired

() // 重啟定時器,保證下一次時延的末尾觸發

timerid = settimeout(timerexpired, remainingwait(time))

}function trailingedge(time)

// 每次 trailingedge 都會清除 lastargs 和 lastthis,目的是避免最後一次函式被執行了兩次

// 舉個例子:最後一次函式執行的時候,可能恰巧是前一次的 trailing edge,函式被呼叫,而這個函式又需要在自己時延的 trailing edge 觸發,導致觸發多次

lastargs = lastthis = undefined

return result

}function

cancel

() {}

function

flush

() {}

function

pending

() {}

function debounced(...args)

if (maxing)

}// 負責一種case:trailing 為 true 的情況下,在前乙個 wait 的 trailingedge 已經執行了函式;

// 而這次函式被呼叫時 shouldinvoke 不滿足條件,因此要設定定時器,在本次的 trailingedge 保證函式被執行

if (timerid === undefined)

return result

}debounced.cancel = cancel

debounced.flush = flush

debounced.pending = pending

return debounced

}複製**

這裡我用文本來簡單描述一下流程:

首次進入函式時因為 lastcalltime === undefined 並且 timerid === undefined,所以會執行 leadingedge,如果此時 leading 為 true 的話,就會執行 func。同時,這裡會設定乙個定時器,在等待 wait(s) 後會執行 timerexpired,timerexpired 的主要作用就是觸發 trailing。

如果在還未到 wait 的時候就再次呼叫了函式的話,會更新 lastcalltime,並且因為此時 isinvoking 不滿足條件,所以這次什麼也不會執行。

時間到達 wait 時,就會執行我們一開始設定的定時器timerexpired,此時因為time-lastcalltime < wait,所以不會執行 trailingedge。

最後,如果不再有函式呼叫,就會在定時器結束時執行 trailingedge。

那麼,回到上面的兩個問題,我的**究竟是**出了問題呢?

為什麼順序圖不對

研究了一下,lodash是比較穩定的在trailing時觸發前一次函式呼叫的,而我的則是每次在 maxwait 時觸發的下一次呼叫。問題就出在對於定時器的控制上。

因為在編碼時考慮到定時器和 maxwait 會衝突的問題,在函式每次被呼叫的時候都會cleartimeout(timer),因此我的trailing判斷其實只對整個執行流的最後一次有效,而非 lodash 所說的trailing控制的是函式在每個wait的最後執行。

而 lodash 並不會清除定時器,只是每次生成新的定時器的時候都會根據 lastcalltime 來計算下一次該執行的時間,不僅保證了定時器的準確性,也保證了對每次trailing的控制。

為什麼最後會觸發兩次

通過打 log 我發現這種觸發兩次的情況非常湊巧,最後一次函式執行的時候,正好滿足前乙個時延的 trailing,然後自己這個 wait 的定時器也觸發了,所以最後又觸發了一次本次時延的 trailing,所以觸發了兩次。

理論上 lodash 也會出現這種情況,但是它在每次函式執行的時候都會刪除 lastargs 和 lastthis,而下次函式執行的時候都會判斷這兩個引數是否存在,因此避免了這種情況。

其實之前就知道debouncethrottle的用途和含義,但是每次用起來都得去看一眼文件,通過這次自己實現以及對原始碼的閱讀,終於做到了了熟於心,也發現自己的**設計能力還是有缺陷,一開始並沒有想的很到位。

寫**的,還是要多寫,多看;慢慢做到會寫,會看;與大家共勉。

lodash中的get方法

lodash中使用頻率最高的,應該就是 get 方法去根據路徑獲取物件的值了。他的使用方式非常簡單 get object,path,defaultvalue 根據 object物件的path路徑獲取值。如果解析 value 是 undefined 會以 defaultvalue 取代。這樣我們可以使...

聊聊replication的方式

本文主要聊一聊主流開源產品的replication方式。replication和partition sharding是分布式系統必備的兩種能力。具體詳見複製 分片和路由.對於海量資料來說,replication一方面可以增加冗餘,保證系統可用性,一方面還可以提公升讀取的效率。本文主要聚焦於repli...

聊聊call apply bind的故事

實際上它們真正的樣子是這樣的 它們幾個的作用都是改變this的指向。bind 與另外兩個的區別則是前者改變this,不立即呼叫函式 而後者改變this,立即呼叫函式。以下例子在非嚴格模式下,注釋的是各個情況this的指向 let test test.foo test.foo.call null,1,...