深刻理解javascript函數進階系列第三篇——函數節流和函數防抖

前面的話

  javascript中的函數大多數狀況下都是由用戶主動調用觸發的,除非是函數自己的實現不合理,不然通常不會遇到跟性能相關的問題。但在一些少數狀況下,函數的觸發不是由用戶直接控制的。在這些場景下,函數有可能被很是頻繁地調用,而形成大的性能問題。解決性能問題的處理辦法就是函數節流和函數防抖。本文將詳細介紹函數節流和函數防抖javascript

 

常見場景

  下面是函數被頻繁調用的常見的幾個場景html

  一、mousemove事件。若是要實現一個拖拽功能,須要一路監聽 mousemove 事件,在回調中獲取元素當前位置,而後重置 dom 的位置來進行樣式改變。若是不加以控制,每移動必定像素而觸發的回調數量很是驚人,回調中又伴隨着 DOM 操做,繼而引起瀏覽器的重排與重繪,性能差的瀏覽器可能就會直接假死。java

  二、window.onresize事件。爲window對象綁定了resize事件,當瀏覽器窗口大小被拖動而改變的時候,這個事件觸發的頻率很是之高。若是在window.onresize事件函數裏有一些跟DOM節點相關的操做,而跟DOM節點相關的操做每每是很是消耗性能的,這時候瀏覽器可能就會吃不消而形成卡頓現象數組

  三、射擊遊戲的 mousedown/keydown 事件(單位時間只能發射一顆子彈)瀏覽器

  四、搜索聯想(keyup事件)app

  五、監聽滾動事件判斷是否到頁面底部自動加載更多(scroll事件)dom

  對於這些狀況的解決方案就是函數節流(throttle)或函數去抖(debounce),核心其實就是限制某一個方法的頻繁觸發函數

 

定時器管理

  在介紹函數防抖和函數節流以前,首先要介紹一下定時器管理性能

  定時器管理有兩種機制:測試

  第一種是隻要當前函數沒有執行完成,任何新觸發的函數都會被忽略,能夠實如今持續觸發事件的狀況下,一段時間內只執行一次事件的效果,即函數節流

  簡易代碼以下

function fn(method, context) {
  //忽略新函數
  if(method.tId){
    return false;
  }
  method.tId = setTimeout(function() {
    method.call(context);
  }, 1000);
}

  第二種是隻要有新觸發的函數,就當即中止執行當前函數,轉而執行新函數,能夠實如今持續觸發事件的狀況下,必定在事件觸發n秒後執行,若是n秒內又觸發了這個事件,則以新的事件的時間爲準,仍是n秒後執行,即函數防抖,簡易代碼以下

function fn(method, context) {
 //中止當前函數
  clearTimeout(method.tId);
  method.tId = setTimeout(function() {
    method.call(context);
  }, 1000);
}

 

函數防抖

  函數防抖,字面上來講,是利用函數來防止抖動。在執行觸發事件的狀況下,元素的位置或尺寸屬性快速地發生變化,形成頁面迴流,出現元素抖動的現象。經過函數防抖,使得元素的位置或尺寸屬性延遲變化,從而減小頁面迴流

  簡單的防抖函數代碼以下,該函數接受2個參數,第一個參數爲須要被延遲執行的函數,第二個參數爲延遲執行的時間

<style>
body {
  margin: 0;
}
.show{
  width: 260px;
  height: 100px;
  font-size: 20px;
  text-align: center;
  line-height: 100px;
  background: lightgreen;
}
</style>
<div class="show" id="show">0</div>
<script>
let count = 0
const oShow = document.getElementById('show')
const changeValue = () => { oShow.innerHTML = count ++ }
const debounce = (fn, wait=30) => {
  return () => {
    clearTimeout(fn.timer)
    fn.timer = setTimeout(fn, wait)
  }
}
oShow.addEventListener('mousemove', debounce(changeValue))
</script>

  效果以下:

  可是,changeValue()方法中的this指向window,下面來修正this指向

const debounce = (fn, wait=30) =>{
  return function() {
    clearTimeout(fn.timer)
    fn.timer = setTimeout(fn.bind(this), wait)
  }
}

  還有一個問題,changeValue()方法中的e爲undefined,下面來修正e的值

const debounce = (fn, wait=30) =>{
  return function() {
    clearTimeout(fn.timer)
    fn.timer = setTimeout(fn.bind(this, ...arguments), wait)
  }
}

  或者,使用apply方法

const debounce = (fn, wait=30) =>{
  return function() {
    clearTimeout(fn.timer)
    fn.timer = setTimeout(() => {
      fn.apply(this, arguments)
    }, wait)
  }
}

 

函數節流

  函數節流,即限制函數的執行頻率,在持續觸發事件的狀況下,間斷地執行函數;實現方法對應定時器管理的第一種策略,只要當前函數沒有執行完成,任何新觸發的函數都會被忽略

const throttle = (fn, wait=100) =>{
  return function() {
    if(fn.timer) return
    fn.timer = setTimeout(() => {
      fn.apply(this, arguments)
      fn.timer = null
    }, wait)
  }
}

 

數組分塊

  在前面關於函數節流和函數防抖的討論中,提供了限制函數被頻繁調用的解決方案。下面將遇到另一個問題,某些函數確實是用戶主動調用的,但由於一些客觀的緣由,這些函數會嚴重地影響頁面性能

  一個例子是建立WebQQ的QQ好友列表。列表中一般會有成百上千個好友,若是一個好友用一個節點來表示,在頁面中渲染這個列表的時候,可能要一次性往頁面中建立成百上千個節點

  在短期內往頁面中大量添加DOM節點顯然也會讓瀏覽器吃不消,看到的結果每每就是瀏覽器的卡頓甚至假死。代碼以下:

var ary = [];
for ( var i = 1; i <= 1000; i++ ){
  ary.push( i );    // 假設 ary 裝載了 1000 個好友的數據
};
var renderFriendList = function( data ){
  for ( var i = 0, l = data.length; i < l; i++ ){
    var div = document.createElement( 'div' );
    div.innerHTML = i;
    document.body.appendChild( div );
  }
};
renderFriendList( ary );

  這個問題的解決方案之一是數組分塊技術,下面的timeChunk函數讓建立節點的工做分批進行,好比把1秒鐘建立1000個節點,改成每隔200毫秒建立8個節點

  數組分塊是一種使用定時器分割循環的技術,爲要處理的項目建立一個隊列,而後使用定時器取出下一個要處理的項目進行處理,接着再設置另外一個定時器

  在數組分塊模式中,array變量本質上就是一個「待辦事宜」列表,它包含了要處理的項目。使用shift()方法能夠獲取隊列中下一個要處理的項目,而後將其傳遞給某個函數。若是在隊列中還有其餘項目,則設置另外一個定時器,並經過arguments.callee調用同一個匿名函數

  數組分塊的重要性在於它能夠將多個項目的處理在執行隊列上分開,在每一個項目處理以後,給予其餘的瀏覽器處理機會運行,這樣就可能避免長時間運行腳本的錯誤。一旦某個函數須要花50ms以上的時間完成,那麼最好看看可否將任務分割爲一系列可使用定時器的小任務

  下面是數組分塊模式的簡易代碼

function chunk(array,process,context){
    setTimeout(function(){
        //取出下一個條目並處理
        var item = array.shift();
        process.call(context,item);
        //若還有條目,再設置另外一個定時器
        if(array.length > 0){
            setTimeout(arguments.callee,100);
        }
    },100);    
}
var data = [1,2,3,4,5,6,7,8,9,0];
function printValue(item){
    var div = document.getElementById('myDiv');
    div.innerHTML += item + '<br>';
}
chunk(data.concat(),printValue);

  下面是數組分塊的詳細代碼,timeChunk函數接受3個參數,第1個參數是建立節點時須要用到的數據,第2個參數是封裝了建立節點邏輯的函數,第3個參數表示每一批建立的節點數量

var timeChunk = function( ary, fn, count ){ 
  var obj,t;
  var len = ary.length;
  var start = function(){
    for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){ 
      var obj = ary.shift();
      fn( obj );
    }
  };
  return function(){
    t = setInterval(function(){
      if ( ary.length === 0 ){ // 若是所有節點都已經被建立好
        return clearInterval( t );
      }
      start();
    }, 200 );    // 分批執行的時間間隔,也能夠用參數的形式傳入
  };
};

  最後進行一些小測試,假設有1000個好友的數據,利用timeChunk函數,每一批只往頁面中建立8個節點

var ary = [];
for ( var i = 1; i <= 1000; i++ ){ 
  ary.push( i );
};
var renderFriendList = timeChunk( ary, function( n ){ 
  var div = document.createElement( 'div' ); 
  div.innerHTML = n;
  document.body.appendChild( div );
}, 8 );
renderFriendList();
相關文章
相關標籤/搜索