函數的防抖和節流是個啥???

曾經面試時候被問到過這個,年少的我一臉無知。。。前端

後來工做中遇到了一個場景:輸入名稱的同時去服務器校驗名稱是否重複,但發現以前的代碼居然都沒作限制,輸入一次發一次請求。簡直忍不了,就在項目的utils里加上了防抖函數。
正好作一個總結,加深印象。面試

函數防抖和節流,都是控制事件觸發頻率的方法。應用場景有不少,輸入框持續輸入,將輸入內容遠程校驗、屢次觸發點擊事件、onScroll等等。
爲了說明問題,假設一個場景:鼠標滑過一個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>

效果是這樣的:
圖片描述閉包

在上邊的場景下,咱們不但願觸發一次就執行一次,這就要用到防抖或節流。下面咱們看一下它們能爲咱們作什麼吧。app

防抖

函數防抖,這裏的抖動就是執行的意思,而通常的抖動都是持續的,屢次的。假設函數持續屢次執行,
咱們但願讓它冷靜下來再執行。也就是當持續觸發事件的時候,函數是徹底不執行的,等最後一次觸發結束的
一段時間以後,再去執行。先看一下效果:函數

圖片描述

分解一下需求:性能

  • 持續觸發不執行
  • 不觸發的一段時間以後再執行

那麼怎麼實現上述的目標呢?咱們先看這一點:在不觸發的一段時間以後再執行,那就須要個定時器呀,定時器裏面調用咱們要執行的函數,將arguments傳入。
封裝一個函數,讓持續觸發的事件監聽是咱們封裝的這個函數,將目標函數做爲回調(func)傳進去,等待一段時間事後執行目標函數this

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

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

function debounce(func, delay) {
  let timeout
  return function() {
    clearTimeout(timeout) // 若是持續觸發,那麼就清除定時器,定時器的回調就不會執行。
    timeout = setTimeout(() => {
      func.apply(this, arguments)
    }, delay)
  }
}

用法:3d

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

節流

節流的意思是讓函數有節制地執行,而不是毫無節制的觸發一次就執行一次。什麼叫有節制呢?就是在一段時間內,只執行一次。
一樣,咱們分解一下:

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

效果是這樣的:

圖片描述

思考一下,持續觸發,並不會執行,可是到時間了就會執行。抓取一個關鍵的點:就是執行的時機。
要作到控制執行的時機,咱們能夠經過一個開關,與定時器setTimeout結合完成。

函數執行的前提條件是開關打開,持續觸發時,持續關閉開關,等到setTimeout到時間了,再把開關打開,函數就會執行了。
咱們看一下代碼怎麼實現:

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

調用的時候:

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

這樣,就實現了節流,節流還能夠用時間間隔去控制,就是記錄上一次函數的執行時間,與當前時間做比較,若是當前時間與上次執行時間的時間差大於一個值,就執行。

說明一下節流時,後面操做中應該是由於run=false 因此才直接return,可是不是在return以前let run=ture直接覆蓋掉以前的false

這裏能夠看一下throttle函數內部,和函數調用的時候。首先看函數內部,分解一下結構:

function throttle(func, deley) {
    return function () {
      // 執行func
    }
}

那調用時候呢?也分解一下:

throttle(function () { // 目標函數內容 }, 1000)

這裏throttle函數執行的結果是其內部return的function的調用。也就是說鼠標通過的事件監聽其實是這個被return的function,不斷持續觸發的是它,而throttle函數只是提供了一個做用域,內部用閉包聲明瞭一個run的開關變量,因爲閉包的存在,run這個變量會一直存在不被銷燬,而let run = true只在這個閉包(能夠理解爲做用域)內只聲明瞭一次,但它不會被持續執行,因此return的函數內部的判斷不會被它覆蓋掉。根據打印結果能夠看出,事實確實是如此:

圖片描述

總結

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

歡迎關注個人公衆號: 一口一個前端,不按期分享我所理解的前端知識

相關文章
相關標籤/搜索