js面試(節流)

平平丶淡淡發表於2024-03-17

一、節流

在JavaScript中,節流(throttle)是一種常用的效能最佳化技術,用於限制某個函式在一定時間內的執行頻率。具體來說,節流函式允許你在一段時間內只執行一次回撥函式,即使在這段時間內觸發了多次事件。這有助於防止因為頻繁觸發事件而導致的效能問題。

節流的實現原理是,在事件被觸發後,一個定時器會被設定。如果在定時器完成之前,相同的事件再次被觸發,那麼這次觸發不做任何處理,直到定時器任務完成後,清空定時器,才能對以後新的觸發事件做出響應。這樣,在一定的時間間隔內,只會對觸發事件做一次處理。

應用場景:

  1. 滾動事件處理:在網頁或應用中,滾動事件可能會非常頻繁地觸發,如果不加以控制,可能會導致效能問題。透過使用節流,可以限制滾動事件處理函式的執行頻率,提高頁面的響應速度和流暢度。
  2. 網路請求:對於需要連續傳送請求的場景,如滾動載入資料或自動完成搜尋,過多的請求不僅會增加伺服器壓力,還可能導致使用者體驗下降。透過節流,可以減少請求的次數,降低伺服器壓力,同時保證資料的及時載入。
  3. 動畫效果:在動畫過程中,如果過度頻繁地重新整理和渲染,可能會造成閃爍和卡頓現象。透過節流,可以控制動畫的渲染頻率,提高動畫的流暢性和使用者體驗。

二、前置準備

  1. 準備一個html檔案和一個throttle.js檔案,throttle.js檔案用來編寫節流函式

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>節流</title>
    </head>
    
    <body>
      <div class="throttle">觸發節流事件</div>
      <script src="./throttle.js"></script>
    </body>
    
    </html>
    
    // throttle.js
    const throttle = () => {};
    
  2. 給div繫結點選事件

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>節流</title>
    </head>
    
    <body>
      <div class="throttle">觸發節流事件</div>
      <script src="./throttle.js"></script>
      <script>
        const clickEvent = function (e) {
          console.log("點選事件觸發", e, this)
        }
        document.querySelector(".throttle").addEventListener("click", clickEvent)
      </script>
    </body>
    
    </html>
    

    image-20240317143334764

  3. 將clickEvent方法傳遞給throttle進行處理

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>節流</title>
    </head>
    
    <body>
      <div class="throttle">觸發節流事件</div>
      <script src="./throttle.js"></script>
      <script>
        const clickEvent = function (e) {
          console.log("點選事件觸發", e, this)
          console.log("執行時間", new Date().getSeconds())
        }
        const throttleClickEvent = throttle(clickEvent, 2000)
        document.querySelector(".throttle").addEventListener("click", throttleClickEvent)
      </script>
    </body>
    
    </html>
    
    // throttle.js
    const throttle = (fun, time) => {
      return fun;
    };
    

    在上面修改了一下clickEvent,用來列印時間。目前的效果和一開始沒什麼區別,下面就開始寫節流函式

三、基礎節流實現

  1. 第一次觸發事件,設定一個定時器,只要這個定時器存在,就不再對觸發事件做出響應。直到定時器任務完成,清空定時器

    // throttle.js
    const throttle = (fun, time) => {
      let timer;
      return function () {
        if (!timer) {
          timer = setTimeout(() => {
            fun();
            timer && clearTimeout(timer);
            timer = null;
          }, time);
        }
      };
    };
    

    image-20240317145108032

    現在頻繁觸發點選事件,但會間隔2秒觸發一次事件處理函式。所以基本實現了節流

  2. 修改this指向

    // throttle.js
    const throttle = (fun, time) => {
      let timer;
      return function () {
        if (!timer) {
          timer = setTimeout(() => {
            fun.apply(this);
            timer && clearTimeout(timer);
            timer = null;
          }, time);
        }
      };
    };
    

    image-20240317145325447

  3. 獲取引數

    // throttle.js
    const throttle = (fun, time) => {
      let timer;
      return function (...args) {
        if (!timer) {
          timer = setTimeout(() => {
            fun.apply(this, args);
            timer && clearTimeout(timer);
            timer = null;
          }, time);
        }
      };
    };
    

    image-20240317145434824

    上面就實現了一個最基本的節流函式

四、立即執行

// throttle.js
const throttle = (fun, time, immediately = false) => {
  let timer;
  return function (...args) {
    if (!timer) {
      // 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發
      if (immediately) {
        fun.apply(this, args);
        timer = setTimeout(() => {
          timer && clearTimeout(timer);
          timer = null;
        }, time);
      } else {
        timer = setTimeout(() => {
          fun.apply(this, args);
          timer && clearTimeout(timer);
          timer = null;
        }, time);
      }
    }
  };
};

image-20240317150604902

五、尾部執行控制

  1. 首先我們修改一下html檔案,方便測試

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>節流</title>
    </head>
    
    <body>
      <div class="throttle">觸發節流事件</div>
      <input type="text" class="throttle-input">
      <script src="./throttle.js"></script>
      <script>
        const clickEvent = function (e) {
          console.log("點選事件觸發", e, this)
          console.log("執行時間", new Date().getSeconds())
        }
    
        const inputEvent = function (e) {
          console.log("表單資料", e, this)
          console.log("執行時間", new Date().getSeconds())
        }
        const throttleClickEvent = throttle(clickEvent, 2000, true)
        document.querySelector(".throttle").addEventListener("click", throttleClickEvent)
        document.querySelector(".throttle-input").addEventListener("input", inputEvent)
      </script>
    </body>
    
    </html>
    

    image-20240317154851639

    目前沒有做節流處理,每次輸入都會觸發

  2. 使用節流函式控制觸發頻率

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>節流</title>
      <style>
        input {
          width: 100%;
        }
      </style>
    </head>
    
    <body>
      <div class="throttle">觸發節流事件</div>
      <input type="text" class="throttle-input">
      <script src="./throttle.js"></script>
      <script>
        const clickEvent = function (e) {
          console.log("點選事件觸發", e, this)
          console.log("執行時間", new Date().getSeconds())
        }
    
        const inputEvent = function (e) {
          console.log("表單資料", this.value)
          console.log("執行時間", new Date().getSeconds())
        }
        const throttleClickEvent = throttle(clickEvent, 2000, true)
        const throttleInputEvent = throttle(inputEvent, 2000, true)
        document.querySelector(".throttle").addEventListener("click", throttleClickEvent)
        document.querySelector(".throttle-input").addEventListener("input", throttleInputEvent)
      </script>
    </body>
    
    </html>
    

    image-20240317155234084

    此時,第一次被觸發了,然後兩秒後,觸發了第二次。但是最後一次觸發事件卻沒有得到響應

  3. 修改節流函式

    // throttle.js
    const throttle = (fun, time, immediately = false, tail = false) => {
      let timer;
      let tailTimer;
      return function (...args) {
        if (!timer) {
          // 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發
          if (immediately) {
            fun.apply(this, args);
            timer = setTimeout(() => {
              timer && clearTimeout(timer);
              timer = null;
            }, time);
          } else {
            timer = setTimeout(() => {
              fun.apply(this, args);
              timer && clearTimeout(timer);
              timer = null;
            }, time);
          }
        } else if (tail) {
          // 如果當前已經觸發了響應事件,並且需要對最後一次觸發做出回應,則將後面觸發呼叫的處理函式加入定時器
          // 每一次有新的觸發事件,則將上一次的定時器給清除,保證始終是最新的觸發事件被加入
          tailTimer && clearTimeout(tailTimer);
          tailTimer = setTimeout(() => {
            fun.apply(this, args);
          }, time);
        }
      };
    };
    

    image-20240317155654229

    此時最後一次就會觸發了。我們再來試一試不立即執行的情況

    const throttleInputEvent = throttle(inputEvent, 2000, false)
    

    image-20240317155933638

    透過控制檯發現,最後一次輸入的數字被列印了兩次。為什麼?

    image-20240317191622241

    首先,最後列印的肯定是放在tailTimer裡面的方法,它在53秒執行,是在51秒的時候觸發的。最後一次執行節流函式是52秒,它是在50秒的時候觸發的。之所以這兩個方法列印了同樣的值,是因為最後一次觸發點選事件是在51秒,兩個定時器裡面的方法,都是在51秒後執行的,等它們執行的時候,都會拿到最後那個最新的值。

    那立即執行為什麼沒有這個問題呢?因為立即執行列印的是當時獲取到值。簡單點說:就是延遲執行,在52秒和53秒都會列印51秒最後一次觸發時候的值。立即執行的話,50秒的時候會最後一次使用timer,此時,直接列印出50秒時候的值,在接下來的2秒之內,timer不會再執行,在這2秒內,觸發的最後一次事件會放進tailTimer裡面。

    所以,對於延遲執行的情況,其實沒有必要將最後一次加入到tailTimer,因為timer會獲取到最後一次的值。

    // throttle.js
    const throttle = (fun, time, immediately = false, tail = true) => {
      let timer;
      let tailTimer;
      return function (...args) {
        if (!timer) {
          // 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發
          if (immediately) {
            fun.apply(this, args);
            timer = setTimeout(() => {
              timer && clearTimeout(timer);
              timer = null;
            }, time);
          } else {
            timer = setTimeout(() => {
              fun.apply(this, args);
              timer && clearTimeout(timer);
              timer = null;
            }, time);
          }
        } else if (tail && immediately) {
          // console.log("尾部定時器當前記錄引數:", args[0].srcElement.value);
          // 如果當前已經觸發了響應事件,並且需要對最後一次觸發做出回應,則將後面觸發呼叫的處理函式加入定時器
          // 每一次有新的觸發事件,則將上一次的定時器給清除,保證始終是最新的觸發事件被加入
          tailTimer && clearTimeout(tailTimer);
          tailTimer = setTimeout(() => {
            fun.apply(this, args);
          }, time);
        }
      };
    };
    

    image-20240317192941966

    ​ 這下列印結果就沒問題了。

    再來測試一下立即執行的情況

    const throttleInputEvent = throttle(inputEvent, 2000, true)
    

    image-20240317193141582

    立即執行的情況,就會透過tailTimer來列印最後的結果了。

六、完整程式碼

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>節流</title>
  <style>
    input {
      width: 100%;
    }
  </style>
</head>

<body>
  <div class="throttle">觸發節流事件</div>
  <input type="text" class="throttle-input">
  <script src="./throttle.js"></script>
  <script>
    const clickEvent = function (e) {
      console.log("點選事件觸發", e, this)
      console.log("執行時間", new Date().getSeconds())
    }

    const inputEvent = function (e) {
      const value = this.value
      console.log("表單資料", value)
      console.log("執行時間", new Date().getSeconds())
    }
    const throttleClickEvent = throttle(clickEvent, 2000, true)
    const throttleInputEvent = throttle(inputEvent, 2000, true)
    document.querySelector(".throttle").addEventListener("click", throttleClickEvent)
    document.querySelector(".throttle-input").addEventListener("input", throttleInputEvent)
  </script>
</body>

</html>
// throttle.js
const throttle = (fun, time, immediately = false, tail = true) => {
  let timer;
  let tailTimer;
  return function (...args) {
    if (!timer) {
      // 如果是立即執行,則直接執行處理函式,然後設定定時器,一段時間後才可以觸發
      if (immediately) {
        fun.apply(this, args);
        timer = setTimeout(() => {
          timer && clearTimeout(timer);
          timer = null;
        }, time);
      } else {
        timer = setTimeout(() => {
          fun.apply(this, args);
          timer && clearTimeout(timer);
          timer = null;
        }, time);
      }
    } else if (tail && immediately) {
      // console.log("尾部定時器當前記錄引數:", args[0].srcElement.value);
      // 如果當前已經觸發了響應事件,並且需要對最後一次觸發做出回應,則將後面觸發呼叫的處理函式加入定時器
      // 每一次有新的觸發事件,則將上一次的定時器給清除,保證始終是最新的觸發事件被加入
      tailTimer && clearTimeout(tailTimer);
      tailTimer = setTimeout(() => {
        fun.apply(this, args);
      }, time);
    }
  };
};

更多的最佳化,比如:取消、獲取返回值可以參考防抖的處理。

相關文章