天天看點

js去抖(Debouncing)和節流(Throttling)

在開發過程中會遇到頻率很高的事件或者連續的事件,如果不進行性能的優化,就可能會出現頁面卡頓的現象,比如:

  1. 滑鼠事件:mousemove(拖曳)/mouseover(劃過)/mouseWheel(滾屏)
  2. 鍵盤事件:keypress(基于ajax的使用者名唯一性校驗)/keyup(文本輸入檢驗、自動完成)/keydown(遊戲中的射擊)
  3. window的resize/scroll事件(DOM元素動态定位)

為了解決這類問題,常常使用的方法就是throttle(節流)和debounce(去抖)。throttle(節流)和debounce(去抖)都是用來控制某個函數在一定時間内執行多少次的解決方案,兩者相似而又不同。

我們要知道一點,浏覽器事件的callback會block浏覽器自身的行為(例如上面提到的resize、scroll...), 也就是比如一個scroll觸發的callback要執行一秒,那麼浏覽器會等callback執行的一秒結束後,才會開始執行自己的scroll行為。這樣的話如果每滑動一次就執行一次, 這個頁面就廢了。 是以要盡量保證callback能立即執行完(即使有更大的運算也要扔到另一個function作為callback)

早在2011年,Twitter 網站抛出了一個問題:向下滾動 Twitter 資訊流的時候,變得很慢,很遲鈍。John Resig 發表了​​一篇部落格​​解釋這個問題,文中解釋到直接給 scroll 事件關聯昂貴的函數,是多麼糟糕的主意。

John(5年前)建議的解決方案是,在 onScroll 事件外部,每 250ms 循環執行一次。簡單的技巧,避免了影響使用者體驗。現如今,有一些稍微高端的方式處理事件。我來結合用例介紹下 Debounce,Throttle 和 requestAnimationFrame 吧。我首次看到 debounce 的 JavaScript 實作是在 2009 年的 ​​John Hann 的博文​​​。不久後,Ben Alman 做了個​​ jQuery 插件​​​(不再維護),一年後 Jeremy Ashkenas 把它加入了 ​​underscore.js​​。而後加入了 Lodash 。

一、throttle和debounce差別:

throttle和debounce的作用就是确認事件執行的方式和時機,以前總是不太清楚兩者的差別,容易把二者弄混。下面就通過兩個簡單的場景描述一下debounce和throttle,以後想到這兩個場景就不會再弄混了:

debounce
假設你正在乘電梯上樓,當電梯門關閉之前發現有人也要乘電梯,禮貌起見,你會按下開門開關,然後等他進電梯; 
如果在電梯門關閉之前,又有人來了,你會繼續開門;
這樣一直進行下去,你可能需要等待幾分鐘,最終沒人進電梯了,才會關閉電梯門,然後上樓。      

是以debounce的作用是,當調用動作觸發一段時間後,才會執行該動作,若在這段時間間隔内又調用此動作則将重新計算時間間隔。

throttle
假設你正在乘電梯上樓,當電梯門關閉之前發現有人也要乘電梯,禮貌起見,你會按下開門開關,然後等他進電梯;  
但是,你是個沒耐心的人,你最多隻會等待電梯停留一分鐘;
在這一分鐘内,你會開門讓别人進來,但是過了一分鐘之後,你就會關門,讓電梯上樓。      

是以throttle的作用是,預先設定一個執行周期,當調用動作的時刻大于等于執行周期則執行該動作,然後進入下一個新的時間周期。

二、簡單實作:

1、首先來看看debounce的實作,根據前面對debounce的描述:

  1. debounce函數會通過閉包維護一個timer
  2. 當同一action在delay的時間間隔内再次觸發,則清理timer,然後重新設定timer
var debounce = function(action, delay) {
    var timer = null;
    
    return function() {
        var self = this, 
              args = arguments;
              
        clearTimeout(timer);
        timer = setTimeout(function() {
            action.apply(self, args)
        }, delay);
    }
}

// example
function resizeHandler() {
    console.log("resize");
}

window.onresize = debounce(resizeHandler, 300);      

可以在Chrome中運作下面的代碼,看看debounce的效果,​​代碼Github連結​​

2、throttle實作

throttle跟debounce的最大不同就是,throttle會有一個閥值,當到達閥值的時候action必定會執行一次。是以throttle的實作可以基于前面的debounce的實作,隻需要加上一個閥值,​​代碼Github連結​​:

var throttleV1 = function(action, delay, mustRunDelay) {
    var timer = null,
          startTime;
          
    return function() {
        var self = this, 
              args = arguments, 
              currTime = new Date();
              
        clearTimeout(timer);
        
        if(!startTime) {
            startTime = currTime;
        }
        
        if(currTime - startTime >= mustRunDelay) {
            action.apply(self, args);
            startTime = currTime;
        }
        else {
            timer = setTimeout(function() {
                action.apply(self, args);
            }, delay);
        }
    };
};      

其實,對于上面的實作可以進心簡化,隻是通過閉包維護一個開始的時間:

var throttleV2 = function(action, delay){
    var statTime = 0;
    
    return function() {
        var currTime = +new Date();
        
        if (currTime - statTime > delay) {
            action.apply(this, arguments);
            statTime = currTime ;
        }
    }
}    

// example
function resizeHandler() {
    console.log("resize");
}

window.onresize = throttleV2(resizeHandler, 300);      

通過前面的介紹,應該對debounce和throttle有一個直覺的認識了:

  • debounce:把觸發非常頻繁的事件合并成一次執行
  • throttle:設定一個閥值,在閥值内,把觸發的事件合并成一次執行;當到達閥值,必定執行一次事件

了解了throttle和debounce之後,下面看看他們的常用場景:

debounce

  • 對于鍵盤事件,當使用者輸入比較頻繁的時候,可以通過debounce合并鍵盤事件處理
  • 對于ajax請求的情況,例如當頁面下拉超過一定傳回就通過ajax請求新的頁面内容,這時候可以通過debounce合并ajax請求事件

throttle

  • 對于鍵盤事件,當使用者輸入非常頻繁,但是我們又必須要在一定時間内(閥值)内執行處理函數的時候,就可以使用throttle。例如,一些網頁遊戲的鍵盤事件
  • 對于滑鼠移動和視窗滾動,滑鼠的移動和視窗的滾動會帶來大量的事件,但是在一段時間内又必須看到頁面的效果。例如對于可以拖動的div,如果使用debounce,那麼div會在拖動停止後一下子跳到目标位置;這時就需要使用throttle
  • 使用者向下滾動無限滾動頁面,需要檢查滾動位置距底部多遠,如果鄰近底部了,我們可以發 AJAX 請求擷取更多的資料插入到頁面中。使用debounce 就不适用了,隻有當使用者停止滾動的時候它才會觸發。實際使用者滾動至鄰近底部時,我們就想擷取内容。是以,使用throttle 可以保證我們不斷檢查距離底部有多遠。

三、示例:

1、向下無限滾動頁面:

<!doctype html>
<html>
<head>
<title>throttle測試</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.5.0/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>

<style type="text/css">
body {
   background: #444444;
   color: white;
   font: 15px/1.51 system, -apple-system, ".SFNSText-Regular", "San Francisco", "Roboto", "Segoe UI", "Helvetica Neue", "Lucida Grande", sans-serif;
   margin:0 auto;
   max-width:600px;
   padding:20px;
}
.item {
  border:4px solid white;
  height:120px;
  width:100%;
  margin-bottom:50px;
  background:#333;
  padding:20px;
}
.color-1 { border-color: #9BFFBB}
.color-2 { border-color: #B9C6FF}
.color-3 { border-color: #FFA3D8}
.color-4 { border-color: #FF8E9B}
</style>
<script type="text/javascript">
$(document).ready(function(){
  
  // Check every 200ms the scroll position
  $(document).on('scroll', _.throttle(function(){
    check_if_needs_more_content();
  }, 300));

  function check_if_needs_more_content() {     
    pixelsFromWindowBottomToBottom = 0 + $(document).height() - $(window).scrollTop() -$(window).height();
    
  // console.log($(document).height());
  // console.log($(window).scrollTop());
  // console.log($(window).height());
  //console.log(pixelsFromWindowBottomToBottom);
    
    
    if (pixelsFromWindowBottomToBottom < 200){
      // Here it would go an ajax request
      $('body').append($('.item').clone()); 
    }
  }
});
</script>
</head>
<body>

<h1>Infinite scrolling throttled</h1>
<div class="item color-1">Block 1</div>
<div class="item color-2">Block 2</div>
<div class="item color-3">Block 3</div>
<div class="item color-4">Block 4</div>
</body>
</html>      

效果如下:

繼續閱讀