JS防抖和節流詳解及utils封裝

題外話:下班騎車路上聽 陳鋼先生 的《人文通識》,講的是啓功先生,講到晚年獨居的那段,差點哭出來。。。

背景

開發中有些事件會頻繁觸發,例如window的resize、scroll,光標行爲mousedown、mousemove,鍵盤行爲keyup、keydown等。頻繁觸發可看下例:https://jsbin.com/lifideyufu/edit?html,output

// 代碼
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
  <style>
    #container{
      width: 100%; height: 200px; line-height: 200px; text-align: center; color: #fff; background-color: #444; font-size: 30px;
    }
  </style>
</head>
<body>
   <div id="container"></div>
   <script>
     var count = 1;
     var container = document.getElementById('container');
     function getUserAction(){
       container.innerHTML = count++;
     }
     container.onmousemove = getUserAction;
   </script>
</body>
</html>

光標在盒子裏稍微一移動,事件就觸發了幾十次,那我們試想,如果這個觸發事件是調用後端接口的話,那就很尷尬了。

爲此,防抖和節流是兩種比較好的解決方案。一般我們項目中,對於某些方法的調用,例如按鈕點擊事件、input的keyup事件等都會加上防護。

釋義

  防抖(debounce)

      觸發事件後在 n 秒內函數只能執行一次,如果在 n 秒內又觸發了事件,則會重新計算函數執行時間。

      思路:每次觸發事件時都取消之前的延時調用方法,重新計時。

不想看詳解的直接上代碼

let debounce_timer: number = 0;
export function debounce(fn: Function, delay: number): Function {
  return function(this: Function) {
    let args = arguments;
    clearTimeout(debounce_timer);
    debounce_timer = window.setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

  節流(throttle)

     連續觸發事件但是在 n 秒中只執行一次函數。節流會稀釋函數的執行頻率。

     思路:每次觸發事件時都判斷當前是否有等待執行的延時函數。

不想看詳解的直接上代碼

const t_config: { timer: number; remaining: number; previous: Date } = {
  timer: 0,
  remaining: 0,
  previous: new Date()
};
export function throttle(fn: Function, delay: number): Function {
  return function(this: Function) {
    let now = new Date(),
      args = arguments;
    t_config.remaining = +now - +t_config.previous;

    if (t_config.remaining >= delay) {
      if (t_config.timer) {
        clearTimeout(t_config.timer);
      }
      fn.apply(this, args);
      t_config.previous = now;
    } else {
      if (!t_config.timer) {
        t_config.timer = window.setTimeout(() => {
          fn.apply(this, args);
          t_config.previous = new Date();
          t_config.timer = 0;
        }, delay - t_config.remaining);
      }
    }
  };
}

詳解

  防抖(debounce)

// 第一版
function debounce(func, wait) {
    var timeout;
    return function () {
        clearTimeout(timeout)
        timeout = setTimeout(func, wait);
    }
}

//調用
container.onmousemove = debounce(getUserAction, 1000);

第一版  問題:調用debounce之後this的指向被改變成Window對象。

// 第二版
function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context)
        }, wait);
    }
}

第二版 問題:調用debounce之後event對象爲undefined了。

// 第三版
function debounce(func, wait) {
    var timeout;
    return function () {
        var context = this;
        var args = arguments;
        clearTimeout(timeout)
        timeout = setTimeout(function(){
            func.apply(context, args)
        }, wait);
    }
}

第三版 前三版都是停止觸發後才執行,就是我們把wait設置爲10s,鼠標移動入盒子,等你不再移動後的10s,count纔會變成1。

測試效果:https://jsbin.com/loqogucuwu/5/edit?html,output 

也就是說函數不會立即執行。下面第四版添加一個參數,可變成立即執行。移入就執行函數,wait過後再移動,也立即執行。

// 第四版
function(func, wait, immediate) {
    var timeout, result;
    return function() {
        var context = this;
        var args = arguments;
        if (timeout) clearTimeout(timeout);
        if (immediate) {
            var callNow = !timeout;
            timeout = setTimeout(()=>{
                timeout = null
            }, wait);
            if (callNow) result = func.apply(context, args);
        } else {
            timeout = setTimeout(()=>{
                func.apply(context, args);
            }, wait);
        }
        return result;
    }    
}

測試效果:https://jsbin.com/vaqowasise/edit?html,output  至此。防抖結束。

  節流(throttle)

   實現節流的方式有兩種:一種是用時間戳,一種是用定時器。

   兩種方法區別:時間戳方法觸發會立即執行,停止觸發就不再執行。

                            定時器方法觸發之後N秒才執行,停止觸發會再執行一次。

   時間戳:記錄一個時間戳(第一次時間戳爲0),獲取當前時間,與記下的時間戳作比較,大於wait,才執行方法。

// 時間戳
function throttle(func, wait){
    var context, args;
    var tag = 0;
    return function(){
        var now = new Date().getTime();
        context = this;
        args = arguments;
        if(now - tag > wait){
            func.apply(context, args);
            tag = now;
        }
    }
}

測試效果:https://jsbin.com/hapijivutu/1/edit?html,output

定時器:觸發事件時設置一個定時器,再次觸發的時候,先判斷是否存在定時器,存在則不執行,等已存在的定時器執行完畢,清空定時器,則可以再設置在執行。

// 定時器
function throttle(func,wait){
    var context, args, timeout;
    var tag = 0;
    return function(){
        context = this;
        args = arguments;
        if(!timeout){
            timeout = setTimeout(()=>{
                timeout = null;
                func.apply(context, args);
            }, wait)
        }
    }
}

參考文章:

https://github.com/mqyqingfeng/Blog/issues/22  

https://github.com/mqyqingfeng/Blog/issues/26

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