Throttle 和 Debounce 的本質(zhì)及一個簡單的實現(xiàn)

就不把這兩個詞翻譯成中文了,直接解釋他們的概念。實際上這兩個東西本質(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ù)體幅骄,首先確定的是肯定會有 lastTriggerTimelastExecutedTime 這兩個變量來作為下一次事件觸發(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偏灿。

上面只是一種模式丹诀,因為 executeOncePerWaitimmediate 這兩個參數(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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市铆遭,隨后出現(xiàn)的幾起案子硝桩,更是在濱河造成了極大的恐慌,老刑警劉巖枚荣,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件碗脊,死亡現(xiàn)場離奇詭異,居然都是意外死亡橄妆,警方通過查閱死者的電腦和手機衙伶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來害碾,“玉大人痕支,你說我怎么就攤上這事÷” “怎么了?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵另绩,是天一觀的道長儒陨。 經(jīng)常有香客問我,道長笋籽,這世上最難降的妖魔是什么蹦漠? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮车海,結(jié)果婚禮上笛园,老公的妹妹穿的比我還像新娘。我一直安慰自己侍芝,他們只是感情好研铆,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著州叠,像睡著了一般棵红。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上咧栗,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天逆甜,我揣著相機與錄音,去河邊找鬼致板。 笑死交煞,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的斟或。 我是一名探鬼主播素征,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了稚茅?” 一聲冷哼從身側(cè)響起纸淮,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎亚享,沒想到半個月后咽块,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡欺税,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年侈沪,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晚凿。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡亭罪,死狀恐怖渴肉,靈堂內(nèi)的尸體忽然破棺而出衅枫,到底是詐尸還是另有隱情涯竟,我是刑警寧澤阳欲,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布爬凑,位于F島的核電站刁卜,受9級特大地震影響两芳,放射性物質(zhì)發(fā)生泄漏驯鳖。R本人自食惡果不足惜肆氓,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一袍祖、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧谢揪,春花似錦蕉陋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至患民,卻和暖如春村视,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酒奶。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工蚁孔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惋嚎。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓杠氢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親另伍。 傳聞我的和親對象是個殘疾皇子鼻百,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容