1. 程式人生 > 程式設計 >淺談JavaScript節流和防抖函式

淺談JavaScript節流和防抖函式

概念

節流函式

間隔固定的時間執行傳入的方法

目的是防止函式執行的頻率過快,影響效能.常見於跟滾動,滑鼠移動事件繫結的功能.

防抖函式

對於接觸過硬體的人也許更好理解,硬體按鈕按下時,由於使用者按住時間的長短不一,會多次觸發電流的波動,加一個防抖函式就會只觸發一次,防止了無意義的電流波動引起的問題.

按鍵防反跳(Debounce)為什麼要去抖動呢?機械按鍵在按下時,並非按下就接觸的很好,尤其是有簧片的機械開關,會在接觸的瞬間反覆的開合多次,直到開關狀態完全改變。

應用在前端時,常見的場景是,輸入框打字動作結束一段時間後再去觸發查詢/搜尋/校驗,而不是每打一個字都要去觸發,造成無意義的ajax查詢等,或者與調整視窗大小繫結的函式,其實只需要在最後視窗大小固定之後再去執行動作.

自己的實現

防抖函式

關鍵點在於每次觸發時都清空延時函式的手柄,只有最後一次觸發不會清空手柄,所以最後一次觸發會等預設的1s後去執行debounce傳入的引數函式f. debounce內部返回的閉包函式,是真正每次被呼叫觸發的函式,不再是原本的f,所以這裡的arguments取閉包函式環境變數中的arguments並在執行f時傳給f,在setTimeout函式的外面取得.

let debounce = function(f,interval = 1000) {
 let handler = null;
 return function() {
  if (handler) {
  clearTimeout(handler);
  }
  let arg = arguments;
  handler = setTimeout(function() {
  f.apply(this,arg);
  clearTimeout(handler);
  },interval)
 }
 }

應用:

let input = document.querySelector('#input');
 input.addEventListener('input',debounce(function(e) {
 console.log("您的輸入是",e.target.value)
 }))

更高階的實現還會考慮到,以leading和trailing作為引數,起始先執行一次函式並消除後面的抖動,還是最後執行一下函式,消除前面的抖動,如同我這裡的例子.後面分析loadash的防抖函式時會詳細解析.

節流函式

let throttle = function(f,gap = 300){
  let lastCall = 0;
  return function(){
  let now = Date.now();
  let ellapsed = now - lastCall;
  if(ellapsed < gap){
   return
  }
  f.apply(this,arguments);
  lastCall = Date.now();
  }
 }

閉包函式在不斷被呼叫的期間,去記錄離上一次呼叫間隔的時間,如果間隔時間小於節流設定的時間則直接返回,不去執行真正被包裹的函式f.只有間隔時間大於了節流函式設定的時間gap,才呼叫f,並更新呼叫時間.

應用:

document.addEventListener('scroll',throttle(function (e) {
 // 判斷是否滾動到底部的邏輯
 console.log(e,document.documentElement.scrollTop);
 }));

lodash原始碼分析

以上是對節流防抖函式最基礎簡單的實現,我們接下來分析一下lodash庫中節流防抖函式的分析.

節流函式的使用

$(window).on('scroll',_.debounce(doSomething,200));
function debounce(func,wait,options) {
 var lastArgs,lastThis,result,timerId,lastCallTime = 0,lastInvokeTime = 0,leading = false,maxWait = false,trailing = true;

 if (typeof func != 'function') {
  throw new TypeError(FUNC_ERROR_TEXT);
 }
 wait = wait || 0;
 if (isObject(options)) {
  leading = !!options.leading;
  maxWait = 'maxWait' in options && Math.max((options.maxWait) || 0,wait);
  trailing = 'trailing' in options ? !!options.trailing : trailing;
 }

 function invokeFunc(time) {
  var args = lastArgs,thisArg = lastThis;

  lastArgs = lastThis = undefined;
  lastInvokeTime = time;
  result = func.apply(thisArg,args);
  return result;
 }

 function leadingEdge(time) {
  console.log("leadingEdge setTimeout")
  // Reset any `maxWait` timer.
  lastInvokeTime = time;
  // Start the timer for the trailing edge.
  timerId = setTimeout(timerExpired,wait);
  // Invoke the leading edge.
  return leading ? invokeFunc(time) : result;
 }

 function remainingWait(time) {
  var timeSinceLastCall = time - lastCallTime,timeSinceLastInvoke = time - lastInvokeTime,result = wait - timeSinceLastCall;
  console.log("remainingWait",result)
  return maxWait === false ? result : Math.min(result,maxWait - timeSinceLastInvoke);
 }

 function shouldInvoke(time) {
  console.log("shouldInvoke")
  var timeSinceLastCall = time - lastCallTime,timeSinceLastInvoke = time - lastInvokeTime;
  console.log("time",time,"lastCallTime",lastCallTime,"timeSinceLastCall",timeSinceLastCall)
  console.log("time","lastInvokeTime",lastInvokeTime,"timeSinceLastInvoke",timeSinceLastInvoke)
  console.log("should?",(!lastCallTime || (timeSinceLastCall >= wait) ||
  (timeSinceLastCall < 0) || (maxWait !== false && timeSinceLastInvoke >= maxWait)))
  // Either this is the first call,activity has stopped and we're at the
  // trailing edge,the system time has gone backwards and we're treating
  // it as the trailing edge,or we've hit the `maxWait` limit.
  return (!lastCallTime || (timeSinceLastCall >= wait) ||
  (timeSinceLastCall < 0) || (maxWait !== false && timeSinceLastInvoke >= maxWait));
 }

 function timerExpired() {
  console.log("timerExpired")
  var time = Date.now();
  if (shouldInvoke(time)) {
  return trailingEdge(time);
  }
  console.log("Restart the timer.",remainingWait(time))
  // Restart the timer.
  console.log("timerExpired setTimeout")
  timerId = setTimeout(timerExpired,remainingWait(time));
 }

 function trailingEdge(time) {
  clearTimeout(timerId);
  timerId = undefined;

  // Only invoke if we have `lastArgs` which means `func` has been
  // debounced at least once.
  console.log("trailing",trailing,"lastArgs",lastArgs)
  if (trailing && lastArgs) {
  return invokeFunc(time);
  }
  lastArgs = lastThis = undefined;
  return result;
 }

 function cancel() {
  if (timerId !== undefined) {
  clearTimeout(timerId);
  }
  lastCallTime = lastInvokeTime = 0;
  lastArgs = lastThis = timerId = undefined;
 }

 function flush() {
  return timerId === undefined ? result : trailingEdge(Date.now());
 }

 function debounced() {
  var time = Date.now(),isInvoking = shouldInvoke(time);
  console.log("time",time);
  console.log("isInvoking",isInvoking);
  lastArgs = arguments;
  lastThis = this;
  lastCallTime = time;

  if (isInvoking) {
  if (timerId === undefined) {
   return leadingEdge(lastCallTime);
  }
  // Handle invocations in a tight loop.
  clearTimeout(timerId);
  console.log("setTimeout")
  timerId = setTimeout(timerExpired,wait);
  return invokeFunc(lastCallTime);
  }
  return result;
 }
 debounced.cancel = cancel;
 debounced.flush = flush;
 return debounced;
 }

ref

https://css-tricks.com/debouncing-throttling-explained-examples/

https://github.com/lodash/lodash/blob/4.7.0/lodash.js#L9840

https://jinlong.github.io/2016/04/24/Debouncing-and-Throttling-Explained-Through-Examples/

以上就是淺談JavaScript節流和防抖函式的詳細內容,更多關於JavaScript節流和防抖函式的資料請關注我們其它相關文章!