防抖和節流(例項講解)

老毛發表於2022-04-21

防抖和節流到底是什麼?

防抖和節流屬於效能優化的知識,它可以有效的降低高頻事件觸發時,你定義的方法的執行次數。

還是沒有感覺???那麼,來看下面的場景:

  1. 使用者在搜尋框輸入關鍵詞(只有當他輸入完成時我們才去向伺服器傳送請求,然後給出搜尋結果)
  2. 自動儲存使用者填寫的表單資料

上面的場景都對應著一個高頻事件,即input或者textarea的onKeyUp事件,我們一般是在使用者觸發這個事件後去向伺服器傳送請求(這樣做的好處是不需要使用者去點選搜尋按鈕,有一種實時查詢的感覺)。

那麼問題來了,當使用者輸入一個要查詢的關鍵詞,可能需要多次按下和抬起鍵盤的按鍵,難道每次onKeyUp的時候我們都要去請求伺服器嗎?顯然不夠優雅(因為如果有大量使用者同時搜尋,伺服器壓力會很大)。而 防抖(debounce) 正是要解決類似這樣的問題。

在瀏覽器中我們經常會遇到類似的事件(如瀏覽器scroll,resize,mousemove...)接下來,我們使用 自動儲存 的場景來說明一下在 JavaScript 中如何實現防抖。

場景描述:使用者在textarea中輸入文字後,要為他自動儲存到伺服器(可以理解為儲存為草稿)這時我們需要做的是 優化 請求伺服器的次數,需要用到防抖函式。

防抖函式

先來看一個常見的錯誤寫法,注意!!!百度中搜出的很多結果都是這個樣子,用了之後就會發現,你的函式還是會立刻執行,並不會延時執行。

function debounce(fn, delay) {
    let timer = null
    return function (args) {
        if (timer) {
            clearTimeout(timer) 
        }
        timer = setTimeout(fn.call(this, args), delay)
    }
}

問題出在 timer = setTimeout(fn.call(this, args), delay) 這一行。

修改成下面的樣子,就可以按設定的delay延時執行了:

function debounce(fn, delay) {
    let timer = null
    return function (args) {
        if (timer) {
            clearTimeout(timer) 
        }
        timer = setTimeout(function() {
            fn.call(this, args)
        }, delay)
    }
}

// 或者
function debounce(fn, delay) {
  let timer = null
    return function (args) {
      if (timer) {
        clearTimeout(timer)
      }
      timer = setTimeout(function() {
        fn(args)
      }, delay)
    }
}

不要小看這小小的區別,它可能會浪費你大量的時間,而且讓你對防抖產生懷疑...

下面貼一個完整的例子,還有 防抖線上演示地址,方便你更好的理解這個場景。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>防抖和節流</title>
    <style>
        .de_wrapper {
          padding: 20px;
          display: flex;
        }
        .col {
            width: 40%;
        }
        .log {
          height: 300px;
          overflow-y: scroll;
          background-color: #fff;
        }
    </style>
</head>
<body>
<div class="de_wrapper">
    <div class="col">
        <h3>未使用防抖(每次按鍵抬起都會觸發儲存)</h3>
        <textarea name="" id="1" cols="30" rows="10" onKeyUp="printLog(event)"></textarea>
        <div id="log" class="log"></div>
    </div>
    <div class="col">
        <h3>使用防抖(停止輸入2秒後儲存)</h3>
        <!-- <textarea name="" id="2" cols="30" rows="10"></textarea> -->
        <textarea name="" id="2" cols="30" rows="10" onKeyUp="debounceLog(event)"></textarea>
        <div id="log1" class="log"></div>
    </div>
</div>
<script>
    let log = null
    let log1 = null

    window.onload = function() {
        log = document.getElementById('log')
        log1 = document.getElementById('log1')

        // 寫法1
        // document.getElementById('2').addEventListener('keyup', function(e) {
        //     debounceLog(e)
        // })

        // 寫法2
        // document.getElementById('2').addEventListener('keyup', debounceLog)

        // 寫法3
        // document.getElementById('2').addEventListener('keyup', debounce(printDebounceLog, 2000))

    } 

    function printLog(e) {
        log.innerText += `keyup 事件觸發【請求伺服器儲存資料...】: ${e.target.value}\n`
    }

    function printDebounceLog(e) {
        log1.innerText += `keyup 事件觸發【請求伺服器儲存資料...】: ${e.target.value}\n`
    }

    let debounceLog = debounce(printDebounceLog, 2000)

    function debounce(fn, delay) {
      let timer = null
        return function (args) {
          if (timer) {
            clearTimeout(timer)
          }
          timer = setTimeout(function() {
            fn(args)
          }, delay)
        }
    }
</script>
</body>
</html>

節流函式

節流函式(throttle)與防抖函式的區別:函式節流無論事件觸發多麼頻繁,在一定時間內只會執行一次回撥;而函式防抖是在高頻事件的最後一次觸發回撥。

節流函式使用場景:一個很形象的例子就是mousedown發射子彈,每秒只能發出一顆子彈,線上演示地址

function throttle(fn, limit) {
  let lastTime
  return function(args) {
    if (!lastTime) {
      fn.apply(this. args)
      lastTime = Date.now()
    } else {
      if ((Date.now() - lastTime) >= limit) {
        fn.apply(this. args)
        lastTime = Date.now()
      }
    }
  }
}
// 頁面結構
<div class="de_wrapper">
    <div class="col">
        <h3>未使用節流(點選按鈕可以瘋狂發射子彈)</h3>
        <div class="sky"></div>
        <button class="fire_btn">發射</button>
      </div>
      <div class="col">
        <h3>使用節流(發射子彈速度會被限制)</h3>
        <div class="sky"></div>
        <button class="fire_btn">發射</button>
    </div>
</div>

<script>

let sky = null
let sky1 = null
let btn = null
let btn1 = null

window.onload = function() { 
    sky = document.querySelectorAll('.sky')[0]
    sky1 = document.querySelectorAll('.sky')[1]
    btn = document.querySelectorAll('.fire_btn')[0]
    btn1 = document.querySelectorAll('.fire_btn')[1]

    btn.addEventListener('click', fire)
    btn1.addEventListener('click', throttle(t_fire, 1000))
} 

function fire() {
  const b = document.createElement('span')
  b.classList.add('bullet')
  sky.appendChild(b)
  setTimeout(() => {
    sky.removeChild(b)
  }, 1000)
}

function t_fire() {
  const b = document.createElement('span')
  b.classList.add('bullet')
  sky1.appendChild(b)
  setTimeout(() => {
    sky1.removeChild(b)
  }, 1000)
}

function throttle(fn, limit) {
  let lastTime
  return function(args) {
    if (!lastTime) {
      fn.apply(this. args)
      lastTime = Date.now()
    } else {
      if ((Date.now() - lastTime) >= limit) {
        fn.apply(this. args)
        lastTime = Date.now()
      }
    }
  }
}

總結

  1. 函式防抖:將多次操作合併為一次操作進行,原理是維護一個計時器,後設定的定時器會取代之前的定時器,如果高頻事件一直在觸發那麼回撥函式一直不會執行。
  2. 函式節流:使得一定時間內只觸發一次函式。原理是通過判斷是否滿足限制時間,滿足則執行。

文章首發於 《IICOOM-個人部落格 防抖和節流》

相關文章