【002】防抖與節流

前言

防抖與節流也是面試經常出現的。今天晚上對此做了一個複習。希望能夠給你帶來一些思考和啓發。

1.防抖

什麼是防抖

  • 防抖:任務頻繁觸發的情況下,只有任務觸發的間隔超過指定間隔的時候,任務纔會執行。

看到過一個很形象的例子,就是上電梯。如果只有你一個人上電梯進入電梯後過一會(加入時間是5秒)門自動關上電梯上行;如果有很多人上電梯電梯門不會在第一個人進去後,然後經過5s電梯門關上。而是等“很多人”都進入電梯後纔會等待5s電梯上行。

拿我們瀏覽網頁來講,上網搜索某個信息的時候。比如我們要搜索:什麼是節流與防抖,那麼只有你一口氣輸入完纔會調用相對的接口,或者輸入部分達到間隔的時長調用相應接口。

防抖的例子

下面舉個栗子加深印象,可以在複製過去自己實踐一下。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>防抖</title>
</head>
<body>
  <button id="debounce">點我防抖!</button>

  <script>
    window.onload = function() {
      // 1、獲取這個按鈕,並綁定事件
      var myDebounce = document.getElementById("debounce");
      myDebounce.addEventListener("click", debounce(sayDebounce));
    }

    // 2、防抖功能函數,接受傳參
    function debounce(fn) {
      // 4、創建一個標記用來存放定時器的返回值
      let timeout = null;
      return function() {
        // 5、每次當用戶點擊/輸入的時候,把前一個定時器清除
        clearTimeout(timeout);
        // 6、然後創建一個新的 setTimeout,
        // 這樣就能保證點擊按鈕後的 interval 間隔內
        // 如果用戶還點擊了的話,就不會執行 fn 函數
        timeout = setTimeout(() => {
        //   fn.call(this, arguments);
        fn();
        }, 1000);
      };
    }

    // 3、需要進行防抖的事件處理
    function sayDebounce() {
      // ... 有些需要防抖的工作,在這裏執行
      console.log("防抖成功!");
    }

  </script>
</body>
</html>

基本形式

對照註釋好好理解

var processor = {
  timeoutId: null, // 相當於延時setTimeout的一個標記,方便清除的時候使用

  // 實際進行處理的方法
  // 連續觸發停止以後需要觸發的代碼
  performProcessiong: function () {
    // 實際執行的代碼
    // 這裏實際就是需要在停止觸發的時候執行的代碼
  },

  // 初始處理調用的方法
  // 在實際需要觸發的代碼外面包一層延時clearTimeout方法,以便控制連續觸發帶來的無用調用
  process: function () {
    clearTimeout(this.timeoutId); // 先清除之前的延時,並在下面重新開始計算時間

    var that = this; // 我們需要保存作用域,因爲下面的setTimeout的作用域是在window,調用不要我們需要執行的this.performProcessiong方法
    this.timeoutId = setTimeout(function () { // 100毫秒以後執行performProcessiong方法
      that.performProcessiong();
    }, 100) // 如果還沒有執行就又被觸發,會根據上面的clearTimeout來清除並重新開始計算
  }
};

// 嘗試開始執行
processor.process(); // 需要重新綁定在一個觸發條件裏

節流

  • 節流:指定時間間隔內只會執行一次任務。

先把下面的代碼複製到編輯器看看效果。不停的點擊kk有什麼不同。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>節流</title>
</head>
<body>

  <button id="throttle">點我節流!</button>

  <script>
    window.onload = function() {
      // 1、獲取按鈕,綁定點擊事件
      var myThrottle = document.getElementById("throttle");
      myThrottle.addEventListener("click", throttle(sayThrottle));
    }

    // 2、節流函數體
    function throttle(fn) {
      // 4、通過閉包保存一個標記
      let canRun = true;
      return function() {
        // 5、在函數開頭判斷標誌是否爲 true,不爲 true 則中斷函數
        if(!canRun) {
          return;
        }
        // 6、將 canRun 設置爲 false,防止執行之前再被執行
        canRun = false;
        // 7、定時器
        setTimeout( () => {
          fn.call(this, arguments);
          // 8、執行完事件(比如調用完接口)之後,重新將這個標誌設置爲 true
          canRun = true;
        }, 1000);
      };
    }

    // 3、需要節流的事件
    function sayThrottle() {
      console.log("節流成功!");
    }

  </script>
</body>
</html>

實現的方法1:設置時間戳

我們設置三個時間,上一次的時間pre,和當前時間,計算兩者時間差。如果時間差大於我們設定的時間戳(第三個時間)就調用相應的方法,如果時間差小於的話就不執行。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>地鐵進站</title>
</head>
<body>
  <button id="addBtn">進站人數+1</button><button id="resetBtn">重置</button>
  <p id="personTotal">旅客總人數:0</p>
  <p id="personNum">進站人數:0</p>
  <script>
    var personNum = 0; // 進站人數
    var personTotal = 0; // 一共來了多少人
    var addBtn = document.getElementById('addBtn'); // 獲取添加人數按鈕
    var personNumP = document.getElementById('personNum'); // 獲取顯示人數的標籤
    var personTotalP = document.getElementById('personTotal'); // 獲取顯示總人數的標籤
    var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕
    /**
     * @method 增加進站人數
     * @description 每個時間間隔執行的方法
     */
    function addPerson() {
      personNum ++;
      personNumP.innerHTML = `進站人數:${personNum}`;
    }
    /**
     * @method 節流方法(時間戳)
     * @param {Function} fn 需要節流的實際方法
     * @param {Number} wait 需要控制的時間長度
     * @description 根據上一次執行的時間,和這一次執行的時間做比較,如果大於控制的時間,就可以執行
     */
    function throttle(fn, wait) {
      var prev = 0; // 第一次執行的時候是0,所以第一次點擊的時候肯定大於這個數,所以會立馬執行
      return function () {
        var context = this;
        var args = arguments;
        var now = Date.now(); // 實際執行的時間
        personTotal ++;
        personTotalP.innerHTML = `旅客總人數:${personTotal}`;
        if (now - prev >= wait) { // 執行的時間是不是比上次執行的時間大於需要延遲的時間,大於,我們就執行
          fn.apply(context, args);
          prev = now; // 執行了以後,重置上一次執行的時間爲剛剛執行這次函數的時間,下次執行就用這個時間爲基準
        }
      }
    }
    /**
     * @method 重置
     */
    function reset() {
      personNum = 0;
      personTotal = 0;
      personNumP.innerHTML = '進站人數:0';
      personTotalP.innerHTML = `旅客總人數:0`;
    }

    addBtn.addEventListener('click', throttle(addPerson, 1000));
    resetBtn.addEventListener('click', reset);
  </script>
</body>
</html>

方法2:利用setTimeout

利用setTimeout設置一個事件間隔,利用閉包設置一個標誌位,當標誌位是true就執行定時器裏面的函數(執行前將標誌位標記爲false),定時器裏面執行相應的函數執行完後將標誌位設置true。這樣就保證了在一定的時間間隔內只執行一次認爲,可以結合我一開始貼上的代碼思考。

注意使用定時器有一點不好就是第一次點擊的時候還是需要等待一段時間纔會執行定時器的函數,講道理第一次是不需要等待的。
所以這兩種方法用哪種取決業務需要

節流的作用

那麼,節流在工作中的應用?

  • 拖拽一個盒子移動,如果使用防抖的話需要一種拖動停止後n秒再執行,那樣豈不是跳動了,所以節流比較合理,平均一段時間執行一次

  • 懶加載要監聽計算滾動條的位置,使用節流按一定時間的頻率獲取。
    用戶點擊提交按鈕,假設我們知道接口大致的返回時間的情況下,我們使用節流,只允許一定時間內點擊一次。

這樣,在某些特定的工作場景,我們就可以使用防抖與節流來減少不必要的損耗。(至於爲什麼可以看我的瀏覽器相關文章重繪和迴流)

方法3:rAF

大多數電腦顯示器的刷新頻率是60Hz,大概相當於每秒鐘重繪60次。大多數瀏覽器都會對重繪操作加以限制,不超過顯示器的重繪頻率,因爲即使超過那個頻率用戶體驗也不會有提升。因此,最平滑動畫的最佳循環間隔是1000ms/60,約等於16.6ms

而setTimeout和setInterval的問題是,它們都不精確。它們的內在運行機制決定了時間間隔參數實際上只是指定了把動畫代碼添加到瀏覽器UI線程隊列中以等待執行的時間。如果隊列前面已經加入了其他任務,那動畫代碼就要等前面的任務完成後再執行

requestAnimationFrame採用系統時間間隔,保持最佳繪製效率,不會因爲間隔時間過短,造成過度繪製,增加開銷;也不會因爲間隔時間太長,使用動畫卡頓不流暢,讓各種網頁動畫效果能夠有一個統一的刷新機制,從而節省系統資源,提高系統性能,改善視覺效果

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>rAF使用</title>
  <style>
    #SomeElementYouWantToAnimate {
      width: 100px;
      height: 100px;
      background-color: #000;
    }
  </style>
</head>
<body>
  <div id="SomeElementYouWantToAnimate"></div>
  <script>
    var start = null;
    var element = document.getElementById('SomeElementYouWantToAnimate');
    element.style.position = 'absolute';
    /**
     * @method 移動我們的小黑方塊
     */
    function step(timestamp) {
      if (!start) start = timestamp;
      var progress = timestamp - start;
      element.style.left = Math.min(progress / 10, 200) + 'px';
      if (progress < 2000) {
        window.requestAnimationFrame(step);
      }
    }

    window.requestAnimationFrame(step);
  </script>
</body>
</html>

參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章