通俗易懂了解函數的防抖和節流


1.前言

在一次面試中被問到:“談一談js中函數的防抖和節流。”,當時菜雞如我的內心:

只能弱弱的說一句沒怎么了解過。后來找到工作后就將這件事拋在腦后,也沒在深究。

就在前幾天維護公司內部代碼的時候,發現這樣一個場景:當用戶在創建東西時,會把用戶輸入的名字發往服務端校驗是否重名,而當時的代碼是監聽了input輸入框的onchange事件,只要用戶一輸入字符,就立即發出請求校驗,這能忍?如果名字有100個字符發100次請求?用戶沒輸完你校驗個毛線啊!

不能忍!優化!必須優化!首先想到的優化思路是:當用戶輸完后我再發請求校驗,但是我又不知道用戶什么時候輸完。那么可以這樣,用戶一直在輸入時,我不請求,當用戶停止輸入3秒后我就認為此時用戶已經輸入完成,這時候再發請求校驗,這樣即可大大的降低請求次數,提高性能。

就在我沾沾自喜的拿着優化方案給Leader看的時候,Leader聽完淡淡的說了一句:函數防抖和節流了解一下。

此時回過神來,原來這就是防抖啊。

2.概念

函數防抖和節流,都是控制事件觸發頻率的方法,通常用戶優化性能。

2.1 函數防抖(debounce)最后一個人說了算

函數防抖,就是指觸發事件后在 n 秒內函數只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。

函數防抖,這里的抖動就是執行的意思,而一般的抖動都是持續的,多次的。假設函數持續多次執行,我們希望讓它冷靜下來再執行。也就是當持續觸發事件的時候,函數是完全不執行的,等最后一次觸發結束的一段時間之后,再去執行。

防抖的中心思想在於:我會等你到底。在某段時間內,不管你觸發了多少次回調,我都只認最后一次。

簡單的說,當一個動作連續觸發,則只執行最后一次。

常見應用場景:

連續的事件,只需觸發一次回調的場景有:

  • 搜索框搜索輸入。只需用戶最后一次輸入完,再發送請求
  • 手機號、郵箱驗證輸入檢測
  • 窗口大小Resize。只需窗口調整完成后,計算窗口大小。防止重復渲染。

2.2 函數節流(throttle) 第一個人說了算

函數節流,就是限制一個函數在一定時間內只能執行一次。

節流的意思是讓函數有節制地執行,而不是毫無節制的觸發一次就執行一次。什么叫有節制呢?就是在一段時間內,只執行一次。

節流中心思想在於:在某段時間內,不管你觸發了多少次回調,我都只認第一次,並在計時結束時給予響應。

常見應用場景:

間隔一段時間執行一次回調的場景有:

  • 滾動加載,加載更多或滾到底部監聽
  • 谷歌搜索框,搜索聯想功能
  • 高頻點擊提交,表單重復提交

2.3 直觀理解

為了方便理解,我們首先通過畫圖感受一下三種環境(正常情況、函數防抖情況 debounce、函數節流 throttle)下,對於mousemove事件回調的執行情況。

豎線的疏密代表事件執行的頻繁程度。可以看到,正常情況下,豎線非常密集,函數執行的很頻繁。而debounce(函數防抖)則很稀疏,只有當鼠標停止移動時才會執行一次。throttle(函數節流)分布的較為均已,每過一段時間就會執行一次。

3.代碼實現

為了說明問題,假設一個場景:鼠標滑過一個div,觸發onmousemove事件,它內部的文字會顯示當前鼠標的坐標。

<style>
    #box {
      width: 1000px;
      height: 500px;
      background: #ccc;
      font-size: 40px;
      text-align: center;
      line-height: 500px;
    }
</style>

<div id="box"></div>

<script>
  const box = document.getElementById('box')
  box.onmousemove = function (e) {
    box.innerHTML = `${e.clientX}, ${e.clientY}`
  }
</script>

效果如下:

3.1 函數防抖(debounce)

我們想要這樣的效果:當鼠標持續移動時,不顯示鼠標坐標,當鼠標停止移動1秒后再顯示鼠標坐標。

分解一下需求:

  • 持續觸發不執行
  • 不觸發的一段時間之后再執行

那么怎么實現上述的目標呢?我們先看這一點:在不觸發的一段時間之后再執行,那就需要個定時器呀,定時器里面調用我們要執行的函數,將arguments傳入。

封裝一個函數,讓持續觸發的事件監聽是我們封裝的這個函數,將目標函數作為回調(func)傳進去,等待一段時間過后執行目標函數。

function debounce(func, delay) {
  return function() {
    setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

第二點實現了,再看第一點:持續觸發不執行。我們先思考一下,是什么讓我們的函數執行了呢?是上邊的setTimeout。OK,那現在的問題就變成了持續觸發,不能有setTimeout。這樣直接在事件持續觸發的時候,清掉定時器就好了。

// func是我們需要包裝的事件回調, delay是每次推遲執行的等待時間
function debounce(func, delay) {
  // 定時器
  let timeout = null;
  return function() {
    // 每次事件被觸發時,都去清除之前的舊定時器,舊定時器的回調就不會執行。
    if(timer) {
        clearTimeout(timeout) 
    }
    timeout = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

用法:

 box.onmousemove = debounce(function (e) {
    box.innerHTML = `${e.clientX}, ${e.clientY}`
  }, 1000)

效果:

說明:

這里debounce函數執行的結果是其內部return的function的調用。也就是說鼠標經過的事件監聽實際上是這個被return的function,不斷持續觸發的是它,而debounce函數內部用閉包聲明了一個timeout的定時器,由於閉包的存在,timeout會被掛載在window對象上,每次鼠標經過,都會先清除掉上次聲明的timeout,直到最后一次鼠標經過,而它的timeout沒有被清除,所以最后一次的定時器才會執行。

3.2 函數節流(throttle)

我們想要這樣的效果:當鼠標持續移動時,不顯示鼠標坐標,每隔一定的時間再顯示鼠標坐標。

同樣,我們再分解一下需求:

  • 持續觸發並不會執行多次
  • 到一定時間再去執行

持續觸發,並不會執行,但是到時間了就會執行。抓取一個關鍵的點:就是執行的時機。要做到控制執行的時機,我們可以通過一個開關,與定時器setTimeout結合完成。

函數執行的前提條件是開關打開,持續觸發時,持續關閉開關,等到setTimeout到時間了,再把開關打開,函數就會執行了。

function throttle(func, delay) {
    let run = true
    return function () {
      if (!run) {
        return  // 如果開關關閉了,那就直接不執行下邊的代碼
      }
      run = false // 持續觸發的話,run一直是false,就會停在上邊的判斷那里
      setTimeout(() => {
        func.apply(this, arguments)
        run = true // 定時器到時間之后,會把開關打開,我們的函數就會被執行
      }, delay)
    }
  }

用法:

box.onmousemove = throttle(function (e) {
  box.innerHTML = `${e.clientX}, ${e.clientY}`
}, 1000)

效果:

4.總結

防抖和節流巧妙地用了setTimeout,來控制函數執行的時機,優點很明顯,可以節約性能,不至於多次觸發復雜的業務邏輯而造成頁面卡頓。

函數防抖,在一段連續操作結束后,處理回調,利用 clearTimeout 和 setTimeout 實現。函數節流,在一段連續操作中,每一段時間只執行一次,頻率較高的事件中使用來提高性能。

函數防抖關注一定時間連續觸發,只在最后執行一次,而函數節流側重於一段時間內只執行一次。
(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM