就不把這兩個詞翻譯成中文了,直接解釋他們的概念。實際上這兩個東西本質(zhì)上是一樣的,作用都是「為了避免某個『事件』在『一個較短的時間段內(nèi)』內(nèi)連續(xù)被觸發(fā)從而引起的其對應的『事件處理函數(shù)』不必要的連續(xù)執(zhí)行」欺栗。那么區(qū)別在哪呢?
先來舉個例子:
Debounce
比如一個頁面的 "resize" 事件征峦,我們對這個事件的處理可能是重新對頁面進行布局或者至少是改變某個 dom 元素的布局迟几,可以想象一般這個事件一旦觸發(fā)就會短時間(比如是 500ms)內(nèi)連續(xù)觸發(fā)多次,然后對應的事件處理函數(shù)(比如叫 handler)會也會被執(zhí)行對應的次數(shù)栏笆,但實際上我們關注的只是 500ms 內(nèi)最后一次 "resize" 事件處理的結(jié)果类腮,于是最開始的一次到倒數(shù)第二次中間的所有 "resize" 都是不需要去處理的,那么我們會怎么做呢蛉加?我們可能會對 handler 做個 500ms 的延時蚜枢,同時在每次 "resize" 觸發(fā)的時候記錄它的「觸發(fā)時間」,在下次 "resize" 的時候比較當前時間和上次觸發(fā)時間针饥,如果時間差小于 500ms 那么我們就把上一次的處理的剩下延時重置為 500ms厂抽,同時將當次的的觸發(fā)時間作為下次觸發(fā)時的參照時間,這樣會造成什么結(jié)果呢丁眼?這樣造成的結(jié)果是:在一個時間段內(nèi)筷凤,如果任意相鄰兩次事件觸發(fā)的間隔小于 500ms,那么不管這整個時間段的長度是多少苞七,也就是說不管事件觸發(fā)了多少次藐守,最終 handler 都只會被執(zhí)行一次挪丢,就是最后的那一次;極端情況下卢厂,如果這個時間段趨于無窮吃靠,那么 handler 一次也得不到執(zhí)行。這種短時間間隔內(nèi)處理多次事件觸發(fā)的機制就是 Debounce足淆。
Throttle
某些情況下對于 Debounce 的處理方式我們可能不滿意,比如對每個 500ms 的間隔的事件的連續(xù)觸發(fā)礁阁,我們想要 handler 至少執(zhí)行一次巧号,可能是在 500ms 的開頭,也可能是在結(jié)尾姥闭,比如是開頭丹鸿,此時我們會怎么做?我們可能會想到每次觸發(fā)事件時棚品,把當次的觸發(fā)時間和上次的「handler 的執(zhí)行時間」(而 debounce 是上次事件的觸發(fā)時間)對比靠欢,那么每次事件的觸發(fā)時間和上次 handler 的執(zhí)行時間會有個差值,如果這個差值大于 500ms铜跑,那么理所應當?shù)孛殴郑瑘?zhí)行 handler 并記錄此時的執(zhí)行時間作為下一次觸發(fā)時的參考時間;如果小于 500ms 锅纺,就什么也不做掷空。這個延時到期了之后執(zhí)行 handler,執(zhí)行 handler 之后的再次觸發(fā)事件時就創(chuàng)建一個新的時長為 500ms 的延遲囤锉。這樣我們就保證了每個 500ms 內(nèi)的多次事件觸發(fā)的第一次總會得到處理坦弟。這種短時間間隔內(nèi)處理多次事件觸發(fā)的機制就是 Throttle。相同情形下官地,10s 中連續(xù)觸發(fā)事件酿傍,任意相鄰兩次觸發(fā)時間間隔小于 500ms,debounce 只會執(zhí)行一次 handler 而 throttle 會執(zhí)行 10(或者 11)次驱入。
二者根本差別
有了上面的例子赤炒,再來總結(jié)下二者的概念,debounce 和 throttle 本質(zhì)上都是「為了避免某個『事件』在『一個較短的時間段內(nèi)』(稱為 delta T)內(nèi)連續(xù)觸發(fā)從而引起的其對應的『事件處理函數(shù)』不必要的連續(xù)執(zhí)行」的一種事件處理機制沧侥。二者的根本的區(qū)別在于 throttle 保證了在每個 delta T 內(nèi)至少執(zhí)行一次可霎,而 debounce 沒有這樣的保證。體現(xiàn)在實現(xiàn)層面上的區(qū)別就是宴杀,每次事件觸發(fā)時參考的「時間點」對于 debounce 來是「上一次事件觸發(fā)的時間」并且在延時沒有結(jié)束時會重置這個延時為 500ms癣朗,而對于 throttle 來講是「上一次 handler 執(zhí)行的時間」并且在延時尚未結(jié)束時不會重置延時。
實現(xiàn)
下面來嘗試著實現(xiàn)一個簡單的能提供這兩種功能的函數(shù)旺罢。我們可以把 handler 的執(zhí)行時機放在每個時間段的開頭或者結(jié)尾旷余,于是每種處理機制都有兩種模式绢记,「debounce + 開頭執(zhí)行」和「debounce + 結(jié)尾執(zhí)行」以及「throttle + 開頭執(zhí)行」和「throttle + 結(jié)尾執(zhí)行」。常用的模式是「debounce + 結(jié)尾執(zhí)行」和「throttle + 開頭執(zhí)行」正卧,「throttle + 結(jié)尾執(zhí)行」也有意義蠢熄,但是「debounce + 開頭執(zhí)行」個人感覺沒什么意義基本可以被「throttle + 開頭執(zhí)行」替代吧?我們打算按照上面分析的先寫一個通用的函數(shù)炉旷,然后 Throttle 和 Debounce 只用配置對應的參數(shù)再生成一個函數(shù)就行签孔。函數(shù)名就叫 debounceOrThrottle
,我們需要傳入的參數(shù)有 fn
(實際的 handler)窘行、wait
(時間間隔)饥追、immediate
(為 true
是表示在時間段的開頭執(zhí)行,否則在末尾執(zhí)行)罐盔、executeOncePerWait
(為 true
時表示 throttle 否則為 debounce)但绕,函數(shù)的返回值是一個新的經(jīng)過包裝的函數(shù),于是得到我們的函數(shù)聲明:
function debounceOrThrottle({ fn, wait = 300, immediate = false, executeOncePerWait = false }) {
// 函數(shù)體
return function() {
// 返回函數(shù)的函數(shù)體
}
}
我們給 wait
惶看、immediate
捏顺、executeOncePerWait
各自一個默認值,這樣的參數(shù)配置表明生成的是一個「debounce + 結(jié)尾執(zhí)行」的模式纬黎。下面我們就先按照這種模式的實現(xiàn)邏輯來補充函數(shù)體幅骄,首先確定的是肯定會有 lastTriggerTime
、lastExecutedTime
這兩個變量來作為下一次事件觸發(fā)時的參考時間本今,然后還會有 context
(用于作為某個對象的方法時提供 this
綁定)昌执、args
(用于保存 fn
的參數(shù))、tId
(用于保存 setTimeout
的返回值诈泼,作為判斷延時是否到期的依據(jù)懂拾,當延時到期即 fn
執(zhí)行后將之再設為 null
)和 result
(用于保存 fn
的返回值),都初始化為 null
铐达。先寫 debounceOrThrottle
的返回值
function() {
context = this
args = arguments
lastTriggerTime = Date.now()
if(!tId) {
tId = setTimeout(anotherFn, wait) // 此處 被 setTimeout 延時的可能除了要執(zhí)行 fn 以外還有其他操作岖赋,
// 故先用 anotherFn 占位
}
return result
}
下面來是 anotherFn
:
const anotherFn = function() {
const last = Date.now() - lastTriggerTime
if(last < wait && last > 0) {
setTimeout(anotherFn, wait - last)
} else {
result = fn.apply(context, args)
context = args = null
tId = null
}
}
綜合起來就得到一個「debounce + 結(jié)尾執(zhí)行」模式的debounceOrThrottle
:
function debounceOrThrottle ({ fn, wait = 300, immediate = false, executeOncePerWait = false }) {
let tId = null
let context = null
let args = null
let lastTriggerTime = null
let result = null
let lastExecutedTime = null
const later = function() {
const last = Date.now() - lastTriggerTime
if(last < wait && last > 0) {
setTimeout(later, wait - last)
} else {
result = fn.apply(context, args)
context = args = null
tId = null
}
}
return function() {
context = this
args = arguments
lastTriggerTime = Date.now()
if(!tId) {
tId = setTimeout(later, wait)
}
return result
}
}
下面我們來過一遍程序執(zhí)行的流程,比如現(xiàn)在 300ms 內(nèi)三次觸發(fā)了事件瓮孙,節(jié)點分別是 0ms, 100ms, 250ms 各節(jié)點函數(shù)執(zhí)行情況:
- 0ms:
lastTriggerTime = 0
(為方便記日此時的Date.now()
為 0 ) ->tId = setTimeout(anotherFn, wait)
->return null
- 100ms:
lastTriggerTime = 100
->return null
- 250ms:
lastTriggerTime = 250
->return null
fn
的執(zhí)行就是這些情況唐断,然后 later
的首次的執(zhí)行時間點為 300ms,第二次執(zhí)行點為 550ms
- 300ms:
last = 300 - 250
->setTimeout(later, 300 - 50)
- 550ms:
last = 550 - 250
->result = fn.apply(context, args); tId = null
->return result
可以知道上面的關鍵點在于每次 tId = null
時會創(chuàng)建一個新的延時杭抠,在這個延時到期之前所有的事件觸發(fā)造成的結(jié)果是更新 lastTriggerTime
然后通過 setTimeout(later, wait - last)
更新了延時脸甘,(只要保持兩次事件觸發(fā)的時間間隔小于 300ms 那么 last < wait && last > 0
就會永遠成立,也即 fn
永遠得不到執(zhí)行)然后當延時到期時 tId = null
偏灿。
上面只是一種模式丹诀,因為 executeOncePerWait
和 immediate
這兩個參數(shù)還沒有用到,下面是應用這兩個參數(shù)之后的完整版:
function debounceOrThrottle ({ fn, wait = 300, immediate = false, executeOncePerWait = false }) {
if (typeof fn !== 'function') {
throw new Error('fn is expected to be a function')
}
let tId = null
let context = null
let args = null
let lastTriggerTime = null
let result = null
let lastExecutedTime = null
const later = function() {
const last = Date.now() - (executeOncePerWait ? lastExecutedTime : lastTriggerTime)
if(last < wait && last > 0) {
setTimeout(later, wait - last)
} else {
if (!immediate) {
executeOncePerWait && (lastExecutedTime = Date.now())
result = fn.apply(context, args)
context = args = null
}
tId = null
}
}
return function() {
context = this
args = arguments
!executeOncePerWait && (lastTriggerTime = Date.now())
const callNow = immediate && !tId
if(!tId) {
executeOncePerWait && (lastExecutedTime = Date.now())
tId = setTimeout(later, wait)
}
if (callNow) {
executeOncePerWait && (lastExecutedTime = Date.now())
result = fn.apply(context, args)
context = args = null
}
return result
}
}
const debounce = ({ fn, wait, immediate }) =>
debounceOrThrottle({
fn,
wait,
immediate
})
const throttle = ({ fn, wait, immediate = true }) =>
debounceOrThrottle({
fn,
wait,
immediate,
executeOncePerWait: true
})
代碼實現(xiàn)參考:https://www.npmjs.com/package/debounce