遊戲陪玩原始碼開發,正確認識節流和防抖

雲豹科技程式設計師發表於2021-12-09

認識防抖和節流函式

防抖和節流的概念最早不是出現在軟體工程中,防抖是出現在電子元件中,節流是出現的流體流動中。

  • 而javascript是事件驅動的,大量的操作會觸發事件,加入到事件佇列中處理
  • 而對於某些頻繁的事件處理會造成遊戲陪玩原始碼效能的損耗,我們就可以通過防抖和節流來限制事件頻繁的發生

1.1. 認識防抖debounce函式

場景:在遊戲陪玩原始碼實際開發中,常常會碰到點選一個按鈕請求網路介面的場景,這時使用者如果因為手抖多點了幾下按鈕,就會出現短時間內多次請求介面的情況,實際上這會造成效能的消耗,我們其實只需要監聽最後一次的按鈕,但是我們並不知道哪一次會是最後一次,就需要做個延時觸發的操作,比如這次點選之後的300毫秒內沒再點選就視為最後一次。這就是防抖函式使用的場景

總結防抖函式的邏輯

  • 當事件觸發時,遊戲陪玩原始碼中相應的函式並不會立即觸發,而是等待一定的時間;
  • 當事件密集觸發時,函式的觸發會被頻繁的推遲;
  • 只有等待了一段時間也沒事件觸發,才會真正的響應函式

1.2 認識節流throttle函式

場景:遊戲陪玩原始碼開發中我們會有這樣的需求,在滑鼠移動的時候做一些監聽的邏輯比如傳送網路請求,但是我們知道document.onmousemove監聽滑鼠移動事件觸發頻率是很高的,我們希望按照一定的頻率觸發,比如3秒請求一次。不管中間document.onmousemove監聽到多少次只執行一次。這就是節流函式的使用場景

總結節流函式的邏輯

  • 當事件觸發時,會執行這個事件的響應函式;
  • 如果這個事件會被頻繁觸發,那麼節流函式會按照一定的頻率來執行;
  • 不管在這個中間有多少次觸發這個事件,執行函式的頻繁總是固定的;

實現防抖函式

2.1 基本實現v-1

const debounceElement = document.getElementById("debounce");
const handleClick = function (e) {
  console.log("點選了一次");
};
// debounce防抖函式
function debounce(fn, delay) {
  // 定一個定時器物件,儲存上一次的定時器
  let timer = null
  // 真正執行的函式
  function _debounce() {
    // 取消上一次的定時器
    if (timer) {
      clearTimeout(timer);
    }
    // 延遲執行
    timer = setTimeout(() => {
      fn()
    }, delay);
  }
  return _debounce;
}
debounceElement.onclick = debounce(handleClick, 300);

2.2 this-引數v-2

上面handleClick函式有兩個問題,一個是this指向的是window,但其實應該指向debounceElement,還一個是無法傳遞傳遞引數。
優化:

const debounceElement = document.getElementById("debounce");
const handleClick = function (e) {
  console.log("點選了一次", e, this);
};
function debounce(fn, delay) {
  let timer = null;
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(() => {
      fn.apply(this, args) // 改變this指向 傳遞引數
    }, delay);
  }
  return _debounce;
}
debounceElement.onclick = debounce(handleClick, 300);

2.3 可選是否立即執行v-3

有些時候在開發遊戲陪玩原始碼時,我們想點選按鈕的第一次就立即執行,該怎麼做呢?
優化:

const debounceElement = document.getElementById("debounce");
const handleClick = function (e) {
  console.log("點選了一次", e, this);
};
// 新增一個immediate引數 選擇是否立即呼叫
function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false; // 是否呼叫過
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    // 如果是第一次呼叫 立即執行
    if (immediate && !isInvoke) {
      fn.apply(this.args);
      isInvoke = true;
    } else {
      // 如果不是第一次呼叫 延遲執行 執行完重置isInvoke
      timer = setTimeout(() => {
        fn.apply(this, args);
        isInvoke = false;
      }, delay);
    }
  }
  return _debounce;
}
debounceElement.onclick = debounce(handleClick, 300, true);

2.4 取消功能v-4

有些時候遊戲陪玩原始碼設定延遲時間很長,在這段時間內想取消之前點選按鈕的事件該怎麼做呢?
優化:

const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");
const handleClick = function (e) {
  console.log("點選了一次", e, this);
};
function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false; 
  function _debounce(...args) {
    if (timer) {
      clearTimeout(timer);
    }
    if (immediate && !isInvoke) {
      fn.apply(this.args);
      isInvoke = true;
    } else {
      timer = setTimeout(() => {
        fn.apply(this, args);
        isInvoke = false;
      }, delay);
    }
  }
	
  // 在_debounce新增一個cancel方法 用來取消定時器
  _debounce.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return _debounce;
}
const debonceClick = debounce(handleClick, 5000, false);
debounceElement.onclick = debonceClick;
cancelElemetnt.onclick = function () {
  console.log("取消了事件");
  debonceClick.cancel();
};

2.5 返回值v-5(最終版本)

最後一個問題,上面handleClick如果有返回值我們應該怎麼接收到呢
優化:用Promise回撥

const debounceElement = document.getElementById("debounce");
const cancelElemetnt = document.getElementById("cancel");
const handleClick = function (e) {
  console.log("點選了一次", e, this);
  return "handleClick返回值";
};
function debounce(fn, delay, immediate = false) {
  let timer = null;
  let isInvoke = false;
  function _debounce(...args) {
    return new Promise((resolve, reject) => {
      if (timer) clearTimeout(timer);
      if (immediate && !isInvoke) {
        try {
          const result = fn.apply(this, args);
          isInvoke = true;
          resolve(result); // 正確的回撥
        } catch (err) {
          reject(err); // 錯誤的回撥
        }
      } else {
        timer = setTimeout(() => {
          try {
            const result = fn.apply(this, args); 
            isInvoke = false;
            resolve(result); // 正確的回撥
          } catch (err) {
            reject(err); // 錯誤的回撥
          }
        }, delay);
      }
    });
  }
  _debounce.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };
  return _debounce;
}
const debonceClick = debounce(handleClick, 300, true);
// 建立一個debonceCallBack用於測試返回的值
const debonceCallBack = function (...args) {
  debonceClick.apply(this, args).then((res) => {
    console.log({ res });
  });
};
debounceElement.onclick = debonceCallBack;
cancelElemetnt.onclick = () => {
  console.log("取消了事件");
  debonceClick.cancel();
};

實現節流函式

3.1 基本實現v-1

這裡說一下最主要的邏輯,只要 這次監聽滑鼠移動事件處觸發的時間減去上次觸發的時間大於我們設定的間隔就執行想要執行的操作就行了
nowTime−lastTime>interval
nowTime:這次監聽滑鼠移動事件處觸發的時間
lastTime:監聽滑鼠移動事件處觸發的時間
interval:我們設定的間隔

const handleMove = () => {
  console.log("監聽了一次滑鼠移動事件");
};
const throttle = function (fn, interval) {
  // 記錄當前事件觸發的時間
  let nowTime;
  // 記錄上次觸發的時間
  let lastTime = 0;
  // 事件觸發時,真正執行的函式
  function _throttle() {
    // 獲取當前觸發的時間
    nowTime = new Date().getTime();
    // 當前觸發時間減去上次觸發時間大於設定間隔
    if (nowTime - lastTime > interval) {
      fn();
      lastTime = nowTime;
    }
  }
  return _throttle;
};
document.onmousemove = throttle(handleMove, 1000);

3.2 this-引數v-2

和防抖一樣,上面的程式碼也會有this指向問題 以及 引數傳遞
優化:

const handleMove = (e) => {
	console.log("監聽了一次滑鼠移動事件", e, this);
};
const throttle = function (fn, interval) {
  let nowTime;
  let lastTime = 0;
  function _throttle(...args) {
    nowTime = new Date().getTime();
    if (nowTime - lastTime > interval) {
      fn.apply(this, args);
      lastTime = nowTime;
    }
  }
  return _throttle;
};
document.onmousemove = throttle(handleMove, 1000);

3.3 可選是否立即執行v-3

上面的函式第一次預設是立即觸發的,如果我們想自己設定第一次是否立即觸發該怎麼做呢?
優化:

const handleMove = (e) => {
  console.log("監聽了一次滑鼠移動事件", e, this);
};
const throttle = function (fn, interval, leading = true) {
  let nowTime;
  let lastTime = 0;
  function _throttle(...args) {
    nowTime = new Date().getTime();
    // leading為flase表示不希望立即執行函式 
    // lastTime為0表示函式沒執行過
    if (!leading && lastTime === 0) {
      lastTime = nowTime;
    }
    if (nowTime - lastTime > interval) {
      fn.apply(this, args);
      lastTime = nowTime;
    }
  }
  return _throttle;
};
document.onmousemove = throttle(handleMove, 3000, false);

3.4 可選最後一次是否執行v-4(最終版本)

如果遊戲陪玩原始碼最後一次監聽的移動事件與上一次執行的時間不到設定的時間間隔,函式是不會執行的,但是有時我們希望無論到沒到設定的時間間隔都能執行函式,該怎麼做呢?
遊戲陪玩原始碼開發的邏輯是:因為我們不知道哪一次會是最後一次,所以每次都設定一個定時器,定時器的時間間隔是距離下一次執行函式的時間;然後在每次進來都清除上次的定時器。這樣就能保證如果這一次是最後一次,那麼等到下一次執行函式的時候就必定會執行最後一次設定的定時器。

const handleMove = (e) => {
  console.log("監聽了一次滑鼠移動事件", e, this);
};
// trailing用來選擇最後一次是否執行
const throttle = function (fn,interval,leading = true,trailing = false) {
  let nowTime;
  let lastTime = 0;
  let timer;
  function _throttle(...args) {
    nowTime = new Date().getTime();
    // leading為flase表示不希望立即執行函式
    // lastTime為0表示函式沒執行過
    if (!leading && lastTime === 0) {
      lastTime = nowTime;
    }
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    if (nowTime - lastTime >= interval) {
      fn.apply(this, args);
      lastTime = nowTime;
      return;
    }
		
    // 如果選擇了最後一次執行 就設定一個定時器
    if (trailing && !timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
        lastTime = 0;
      }, interval - (nowTime - lastTime));
    }
  }
  return _throttle;
};
document.onmousemove = throttle(handleMove, 3000, true, true);

本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2846779/,如需轉載,請註明出處,否則將追究法律責任。

相關文章