本文同步自我的blog前段時間團隊內部搞了乙個**訓練營,大家組織在一起實現
lodash
的throttle
和debounce
,實現起來覺得並不麻煩,但是最後和官方的一對比,發現功能的實現上還是有差距的,為了尋找我的問題,把官方原始碼閱讀了一遍,本文是我閱讀完成後的一篇總結。
注:本文只會列出比較核心部分的**和注釋,如果對全部的原始碼有興趣的歡迎直接看我的repo:
throttle
:將乙個函式的呼叫頻率限制在一定閾值內,例如 1s 內乙個函式不能被呼叫兩次。
debounce
:當呼叫函式n秒後,才會執行該動作,若在這n秒內又呼叫該函式則將取消前一次並重新計算執行時間,舉個簡單的例子,我們要根據使用者輸入做suggest,每當使用者按下鍵盤的時候都可以取消前一次,並且只關心最後一次輸入的時間就行了。
lodash
對這兩個函式又增加了一些引數,主要是以下三個:
這裡直接劇透一下,其實我自己的**實現放在我的repo裡,大家有興趣的可以看下。之前說過我的實現和throttle
就是設定了maxwait
的debounce
,所以我這裡也只會介紹debounce
的**,聰明的讀者們可以自己思考一下為什麼。
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,而下次函式執行的時候都會判斷這兩個引數是否存在,因此避免了這種情況。
其實之前就知道debounce
和throttle
的用途和含義,但是每次用起來都得去看一眼文件,通過這次自己實現以及對原始碼的閱讀,終於做到了了熟於心,也發現自己的**設計能力還是有缺陷,一開始並沒有想的很到位。
寫**的,還是要多寫,多看;慢慢做到會寫,會看;與大家共勉。
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,...