防抖與節流,不要再分不清啦!

本篇文章主要介紹防抖和節流的原理,以及它們的區別。

防抖與節流的問題總是會在面試中出現(然而我并沒有遇到),如果你在面試前有背書,那肯定能過這題的,但如果現實開發中用的不多的話,估計就很快忘記了怎么寫來著(我就是這樣)。究其原因就是沒徹底弄清楚這兩個的原理與區別,所以準備這次來好好梳理一下。

我們知道前端開發中會遇到頻繁觸發的事件,比如keyup、keydown事件,mousedown、mousemove事件,還有window 的 resize、scroll等。如果任由用戶頻繁觸發此類事件,將帶來極大的性能消耗,或可能導致頁面卡頓。作為有前途的前端人,我們有必要掌握優化技巧,一般解決這類問題的方法也就是防抖節流了。

那么問題也就來了,我們可以去網上搜到相關插件,也能搜到很多優秀的實現源碼,那到底什么場景用防抖,什么場景用節流呢?我們慢慢來看。

防抖(debounce)

先看防抖,用一句話概括防抖就是:觸發高頻事件后n秒內函數只會執行一次,如果n秒內高頻事件再次被觸發,則重新計算時間

說的通俗點就是:你盡管頻繁觸發事件,但我一定是在觸發事件的n秒后才執行,如果在前一個事件觸發的n秒內又重新觸發了這個事件,那就以新的事件的時間為準,n 秒后才執行。核心點就是,要等你在觸發事件后的n秒內不再重新觸發事件,我才執行

我先放一段基本的防抖函數源碼:

// fn 函數傳入用戶方法
// delay 延遲執行的時間,默認 500ms
function debounce(fn, delay = 500) {
  let timer = null
  return function() {
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(() => {
      fn.apply(this, arguments)
      timer = null
    }, delay)
  }
}

社區有很多文章寫了關于防抖函數的實現,每個人的實現方式可能都有細微區別,但其核心部分也就是上面這段了,對我而言夠用,后面會詳細說這段代碼?,F在我們舉例一個常見場景來應用一下這段代碼,我們來監聽一下 input元素的 keyup事件,每次釋放鍵盤時打印輸入值,我們先不加入防抖

<body>
  <input id="input" type="text" />

  <script>
    let input = document.getElementById('input')

    input.addEventListener('keyup', function() {
      console.log(input.value)
    })
  </script>
</body>

運行上面代碼,可以看到每次釋放鍵盤時控制臺都在打印,頻率很高,但因為要執行的操作只是簡單的打印,所以感受不到性能的消耗。而現實開發里時常要執行的操作是ajax請求,假設 1 秒觸發了 60 次,每個請求回調就必須在 1000 / 60 = 16.67ms 內完成,否則可能就會出現卡頓。所以優化這段操作很有必要,我們來應用前面的防抖函數:

  <body>
    <input id="input" type="text" />

    <script>
      let input = document.getElementById('input')

      input.addEventListener(
        'keyup',
        debounce(function() { // 此處通過 debounce 返回用戶操作函數
          console.log(input.value)
        }, 1000)
      )
    </script>
  </body>

加入防抖的效果大家是可以預見的,它抑制住了高頻操作,此時用戶持續輸入,并在 1s 內重新輸入時是不會觸發打印操作的,核心就在這個 1s 內,如果用戶停止輸入并超過 1s ,則會執行打印。我們可以結合上面的debounce函數源碼來分析一下流程:

  1. 當輸入第一個字符,并第一次觸發keyup時,timer為null,所以開始新的定時任務,1秒后執行打印操作,并晴空timer;
  2. 在 1 秒內,用戶又輸入了第二個字符,再次觸發事件,此時定時器保存了上一次的任務,所以執行clearTimeout(timer)清空了定時器,并重新賦值新的定時任務;
  3. 后續用戶持續輸入時,反復執行上一步的操作;
  4. 當用戶停止輸入時,經過 1 秒后,則終于可以執行定時器里的任務。

以前不知道為什么這么寫,現在了解了,記住就行了。另外debounce函數中有一個問題一直被人問起,就是為什么要fn.apply(this, arguments)這樣,而不是直接fn()這樣。其實不使用apply也是可以的,但為了程序的穩定性,還是加入比較好,畢竟又不麻煩。加入apply后解決了兩個不穩定因素:

  1. 不使用防抖函數時,在fn中打印this,本例中指向的是<input id="input" type="text">,而在加入防抖函數后,指向的是Window對象,所以要手動改正 this 指向。
  2. 事件處理函數中會提供事件對象 event,使用防抖函數前后會改變事件對象。比如例子中,使用防抖前,event指向的是 KeyboardEvent對象,加入防抖后則變成 undefined了,所以也要手動傳入參數。

這些都是js基礎,還是需要打牢的。如果源碼中的定時器里不是箭頭函數,就需要這樣寫了:

function debounce(fn, delay = 1000) {
  let timer = null

  return function(...args) {  // 此處顯示定義出參數對象
    let context = this // 緩存 this 對象

    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(function() { 
      fn.apply(context, args) // 注意改變
      timer = null
    }, delay)
  }
}

節流(throttle)

也用一句話概括節流:高頻事件觸發,但在n秒內只會執行一次,所以節流會稀釋函數的執行頻率。

同樣是抑制高頻事件觸發,與防抖的區別在于它不需要用戶停頓,而是在持續觸發的過程中每隔 n 秒執行一次。

實現節流一般有兩個方向,一是使用時間戳,而是使用定時器。

既然是為了比較,那還是使用上面的例子,即監聽keyup事件。我們先看用時間戳來實現節流

// fn 函數傳入用戶方法
// wait 間隔執行時間,默認 500ms
function throttle(fn, wait=500) {
  // 初始時間點
  let previous = 0
  return function(...args) {
    let context = this
    let now = +new Date() // 當前時間戳
    if (now - previous > wait) {
      fn.apply(context, args)
      previous = now
    }
  }
}

當我們應用這個方法時,即:

input.addEventListener(
  'keyup',
  throttle(function() {
    console.log(input.value)
  }, 1000)  // 便于演示,設定wait為 1秒
)

此時瀏覽器運行代碼,在input框持續輸入時,會發現每隔 1 秒就會打印值。我們來梳理一下流程:

  1. 輸入第一個字符時,進入節流邏輯,時間戳肯定大于 1 秒,所以立刻執行打印操作,同時將 previous 設定為當前時間戳;
  2. 持續輸入,間隔時間小于 1 秒時,不執行操作,previous不變,now一直在增長;
  3. now增長到與previous的差值大于 1000 時,執行打印,更新previous;
  4. 如此往復,每隔 1 秒打印一次。而最后輸入的值則不會被打印,因為持續的過程中,最后一次的差值還沒到1000就停止輸入了,超過 1000 時,則是算重新第一次輸入了。我說的可能不好明白,自己走一遍流程就清楚了。

由此,上面的節流方案可以做到限制高頻觸發事件,它的特點是:使用時間戳方式實現的節流,在第一次觸發時會立刻執行,而停止觸發后沒有辦法再執行事件。

現在我們再來試試使用定時器實現的節流方式,放上源碼:

// fn 函數傳入用戶方法
// wait 間隔執行時間,默認 500ms
function throttle(fn, wait) {
  let timeout
  return function(...args) {
    if (!timeout) {
      timeout = setTimeout(() => {
        fn.apply(this, args)
        timeout = null
      }, wait)
    }
  }
}

使用方法是一樣的:

input.addEventListener(
  'keyup',
  throttle(function() {
    console.log(input.value)
  }, 1000)  // 便于演示,設定wait為 1秒
)

此時運行效果依舊是每隔 1 秒執行一次,但也稍有區別,再來梳理一下這個流程:

  1. 輸入第一個字符時,進入節流邏輯,初始定時器無值,所以賦值新的定時任務,1 秒后執行;
  2. 此時用戶在持續輸入,但因為第 1 步定時器已經被賦值了,所以不重新賦值了,函數不執行邏輯;
  3. 此時 1 秒已經過去了,第一步中的定時任務觸發,執行打印操作,清空定時器;
  4. 繼續輸入時,timeout定時器因為被清空了,所以重新賦值,走第 1 步中的邏輯;
  5. 如此往復,總是間隔 1 秒執行一次。可以發現第一次觸發事件時不會立刻執行,而停止輸入時,最后還會執行一次。

自己多過幾遍流程就會很清晰了。

總結

來做個總結:

防抖與節流的區別

我不想從定義上說區別,直接從使用結果上比較區別:

  • 使用防抖:持續觸發高頻事件時,只要觸發時間間隔小于設定的時間閥值,不管持續多久都不會執行用戶操作,只有當停頓時間超過設定的時間閥值時,才會執行一次操作。
  • 使用節流:持續觸發高頻事件時,每隔一段時間就觸發一次操作,不需要“停頓”,這個一段時間是指你設定的時間閥值。所以在這個持續的過程中,會多次觸發操作,而防抖是一個持續過程后只觸發一次。

所以何時使用防抖,何時使用節流,全看你需要的效果,而效果就是上面總結的。

節流兩種實現方式的區別

  • 時間戳方式:事件會立刻執行,事件停止觸發后沒有辦法再執行。
  • 定時器方式:事件會在 n 秒后第一次執行,事件停止觸發后依然會再執行一次事件。

當然有時候這兩種節流方式可能都不能滿足需求,比如你既想要能夠立即執行,也要結束時還能執行一次,又比如你想要自己控制它開始和結束的狀態,不用怕,社區里都能找到你要的,況且我們還有 Lodash 這樣優秀的插件。我在這里只是想要介紹一下他們的原理和區別。

如有不對之處,望指正,謝謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,882評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,208評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,746評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,666評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,477評論 6 407
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,960評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,047評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,200評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,726評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,617評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,807評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,327評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,049評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,425評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,674評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,432評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,769評論 2 372