跟着 underscore 學節流

  • 2019 年 10 月 4 日
  • 筆記

更多內容請參考:我的博客

在上一篇文章中,我們了解了為什麼要限制事件的頻繁觸發,以及如何做限制:

  1. debounce 防抖
  2. throttle 節流

上次已經說過防抖的實現了,今天主要來說一下節流的實現。

節流

節流的原理很簡單:

如果你持續觸發事件,每隔一段時間,只執行一次事件。

根據首次是否執行已經結束後知否執行,效果有所不同,實現的方式也有所不同。 我們用leading代表首次是否執行,trailing 代表結束後是否再執行一次。

關於節流的實現,有兩種主流的實現方式,一種是使用時間戳,一種是設置定時器。

使用時間戳

讓我們來看第一種方法:使用時間戳,當觸發事件的時候,我們取出當前的時間戳,然後減去之前的時間戳(最一開始值設為0),如果大於設置的時間周期,就執行函數,然後更新時間戳為當前的時間戳,如果小於,就不執行。

看了這個表述,讓我們來寫第一版的代碼:

function throttle(func, wait) {    var context,args;    var previous = 0;      return function() {       var now = +new Date();       context = this;       args = arguments;       if(now - previous > wait) {           func.apply(context,args);           previous = now;        }    }  }

例子依然是用講 debounce 中的例子,如果你要使用:

container.onmousemove = throttle(getUserAction,1000)

效果演示如下:

我們可以看到:當鼠標移入的時候,事件立即執行,每過1s 會執行一次,如果再 4.2s 停止觸發,以後不會再執行事件。

使用定時器

接下來,我們講講第二種實現方式,使用定時器。

當觸發事件的時候,我們設置一個定時器,再觸發事件的時候,如果定時器存在,就不執行,直到定時器執行,然後執行函數,清空定時器,這樣就可以設置下個定時器。

function throttle(func, wait) {    var timeout;    var previous = 0;      return function(){      context = this;      args = arguments;      if(!timeout){         timeout = setTimeout(function(){           timeout = null;           func.apply(context, args)         },wait)      }    }  }

為了讓效果更加明顯,我們設置wait 的時間為 3s,效果演示如下:

我們可以看到:當鼠標移入的時候,事件不會立刻執行,晃了 3s 後終於執行了一次,此後每 3s 執行以下,當數字顯示為 3 的時候,立刻移除鼠標,相當於大約 9.2s 的時候停止觸發,但是依然會在第 12s 的時候執行一次事件。

所以比較兩個方法:

  1. 第一種事件會立刻執行,第二種事件會在 n 秒後第一次執行
  2. 第一種事件停止觸發後沒有辦法再執行事件,第二種事件停止觸發後依然會再執行一次事件

雙劍合璧

那我們想要一個什麼樣的呢?

有人就說了:我想要一個有頭有尾的!就是鼠標移入能立刻執行,停止觸發的時候還能再執行一次!

所以我們綜合兩者的優勢,然後雙劍合璧,寫一版代碼:

function throttle(func, wait) {      var timeout, context, args, result;      var previous = 0;        var later = function() {          previous = +new Date();          timeout = null;          func.apply(context, args)      };        var throttled = function() {          var now = +new Date();          //下次觸發 func 剩餘的時間          var remaining = wait - (now - previous);          context = this;          args = arguments;           // 如果沒有剩餘的時間了或者你改了系統時間          if (remaining <= 0 || remaining > wait) {              if (timeout) {                  clearTimeout(timeout);                  timeout = null;              }              previous = now;              func.apply(context, args);          } else if (!timeout) {              timeout = setTimeout(later, remaining);          }      };      return throttled;  }

效果演示如下:

我們可以看到:鼠標移入,事件立刻執行,晃了 3s,事件再一次執行,當數字變成 3 的時候,也就是 6s 後,我們立刻移出鼠標,停止觸發事件,9s 的時候,依然會再執行一次事件。

優化

但是我有時也希望無頭有尾,或者有頭無尾,這個咋辦?

那我們設置個 options 作為第三個參數,然後根據傳的值判斷到底哪種效果,我們約定:

leading:false 表示禁用第一次執行 trailing: false 表示禁用停止觸發的回調

我們來改一下代碼:

function throttle(func, wait, options) {      var timeout, context, args, result;      var previous = 0;      if (!options) options = {};        var later = function() {          previous = options.leading === false ? 0 : new Date().getTime();          timeout = null;          func.apply(context, args);          if (!timeout) context = args = null;      };        var throttled = function() {          var now = new Date().getTime();          if (!previous && options.leading === false) previous = now;          var remaining = wait - (now - previous);          context = this;          args = arguments;          if (remaining <= 0 || remaining > wait) {              if (timeout) {                  clearTimeout(timeout);                  timeout = null;              }              previous = now;              func.apply(context, args);              if (!timeout) context = args = null;          } else if (!timeout && options.trailing !== false) {              timeout = setTimeout(later, remaining);          }      };      return throttled;  }

取消

在 debounce 的實現中,我們加了一個 cancel 方法,throttle 我們也加個 cancel 方法:

...  throttled.cancel = function() {      clearTimeout(timeout);      previous = 0;      timeout = null;  }  ...

注意

我們要注意 underscore 的實現中有這樣一個問題:

那就是 leading:false 和 trailing: false 不能同時設置。

如果同時設置的話,比如當你將鼠標移出的時候,因為 trailing 設置為 false,停止觸發的時候不會設置定時器,所以只要再過了設置的時間,再移入的話,就會立刻執行,就違反了 leading: false,bug 就出來了,所以,這個 throttle 只有三種用法:

container.onmousemove = throttle(getUserAction, 1000);  container.onmousemove = throttle(getUserAction, 1000, {      leading: false  });  container.onmousemove = throttle(getUserAction, 1000, {      trailing: false  });

至此我們已經完整實現了一個 underscore 中的 throttle 函數,恭喜,撒花!

參考文章:https://github.com/mqyqingfeng/Blog/issues/26

Exit mobile version