防抖和節(jié)流

防抖與節(jié)流

前言

我們 JavaScript 中有一些事件,比如瀏覽器的 resize、scroll 事件,鼠標(biāo)的 mousemove、mouseover 事件以及輸入框的 keypress、keyup 事件,它們?cè)谟|發(fā)的時(shí)候會(huì)不斷調(diào)用事件綁定的回調(diào)函數(shù),極大的浪費(fèi)資源,降低前端性能。為了優(yōu)化用戶體驗(yàn),我們需要對(duì)這類事件進(jìn)行調(diào)用次數(shù)的限制。我們可以使用防抖與節(jié)流來(lái)降低事件的觸發(fā)頻率。

防抖

1、定義

當(dāng)連續(xù)觸發(fā)事件時(shí),事件被觸發(fā) N 秒后才執(zhí)行回調(diào),否則如果在這 n 秒內(nèi)又被觸發(fā),則重新計(jì)時(shí)

例:鼠標(biāo)移動(dòng)事件
正常情況:從開始監(jiān)聽到鼠標(biāo)不再移動(dòng)打印出無(wú)數(shù)次
防抖處理:只在鼠標(biāo)不再移動(dòng)后 N 秒打印出鼠標(biāo)最后停止的位置(N 秒為人為設(shè)置)

2、為什么要防抖?

在某一函數(shù)在短時(shí)間內(nèi)被連續(xù)觸發(fā)導(dǎo)致占用大量性能使得系統(tǒng)卡頓,而我們并不真正需要連續(xù)觸發(fā)這個(gè)函數(shù)。我們需要的是連續(xù)觸發(fā)中最后一次的結(jié)果,這時(shí)我們使用防抖來(lái)優(yōu)化函數(shù)的觸發(fā),達(dá)到減少占用性能,防止連續(xù)觸發(fā)導(dǎo)致的卡頓。

3、防抖的實(shí)現(xiàn)

// 防抖
function debounce(fun, delay) {
  return function (args) {
    let that = this;
    let _args = args;
    clearTimeout(fun.id);
    fun.id = setTimeout(function () {
      fun.call(that, _args);
    }, delay);
  };
}

4、示例

移動(dòng)鼠標(biāo),更新數(shù)字,數(shù)字記錄調(diào)用事件回調(diào)次數(shù)

4.1 正常調(diào)用

每次移動(dòng)都會(huì)調(diào)用回調(diào)。

// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = callback;
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
正常調(diào)用

4.2 防抖調(diào)用

每次移動(dòng)間隔超過 1000ms 才會(huì)調(diào)用回調(diào),反之重新計(jì)算時(shí)間。

// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = dobounce(callback, 1000);
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
防抖實(shí)現(xiàn)

4.3 防抖優(yōu)化

首次調(diào)用立即執(zhí)行

// 防抖
function debounce(fun, delay, immediate) {
  return function (args) {
    let that = this;
    let _args = args;
    clearTimeout(fun.id);
    if (immediate) {
      // 如果已經(jīng)執(zhí)行過,不再執(zhí)行
      const callNow = !fun.id;
      fun.id = setTimeout(function () {
        fun.id = null;
      }, delay);
      if (callNow) fun.call(that, _args);
    } else {
      fun.id = setTimeout(function () {
        fun.call(that, _args);
      }, delay);
    }
  };
}
防抖優(yōu)化

節(jié)流

1、定義

當(dāng)連續(xù)觸發(fā)事件時(shí),每隔 N 時(shí)間,觸發(fā)一次函數(shù)

例:監(jiān)聽鼠標(biāo)在屏幕中的位置并打印,連續(xù)不斷的移動(dòng)鼠標(biāo)一段時(shí)間,獲取鼠標(biāo)的移動(dòng)軌跡
正常情況:從開始監(jiān)聽到鼠標(biāo)不再移動(dòng)打印出無(wú)數(shù)次,(軌跡最為精細(xì))
節(jié)流處理:當(dāng)我們不需要這樣精確的軌跡時(shí),每隔 N 秒觸發(fā)一次函數(shù)打印鼠標(biāo)位置,得到較為粗糙的鼠標(biāo)軌跡(N 秒為人為設(shè)置)

2、為什么要節(jié)流?

同樣的,也是為了防止在某一函數(shù)在短時(shí)間內(nèi)被連續(xù)觸發(fā)導(dǎo)致占用大量性能使得系統(tǒng)卡頓,而我們并不真正需要連續(xù)觸發(fā)這個(gè)函數(shù)。我們需要的是穩(wěn)定的每隔一段時(shí)間觸發(fā)一次,這時(shí)我們使用節(jié)流來(lái)優(yōu)化函數(shù)的觸發(fā),達(dá)到減少占用性能,防止連續(xù)觸發(fā)導(dǎo)致的卡頓。

3、節(jié)流的實(shí)現(xiàn)

// 節(jié)流
function throttle(fun, delay) {
  let last, deferTimer;
  return function (args) {
    let that = this;
    let _args = arguments;
    let now = +new Date();
    // 保證第一次執(zhí)行
    if (last && now < last + delay) {
      clearTimeout(deferTimer);
      deferTimer = setTimeout(function () {
        last = now;
        fun.apply(that, _args);
      }, delay);
    } else {
      last = now;
      fun.apply(that, _args);
    }
  };
}

4、示例

移動(dòng)鼠標(biāo),更新數(shù)字,數(shù)字記錄調(diào)用事件回調(diào)次數(shù)

4.1 節(jié)流調(diào)用

首次調(diào)用立即執(zhí)行,每隔 1000ms 調(diào)用一次。

// 容器
const container = document.getElementById("container");
// 數(shù)字容器
const numid = document.getElementById("num");
let num = 0;
function callback() {
  num += 1;
  numid.innerHTML = num;
  console.timeEnd("start");
}
const wrapFun = throttle(callback, 1000);
container.addEventListener("mousemove", function (e) {
  console.time("start");
  wrapFun();
});
節(jié)流調(diào)用

附加

lodash 的防抖與節(jié)流實(shí)現(xiàn)

// 防抖
function debounce(func, wait, options) {
  var lastArgs,
    lastThis,
    maxWait,
    result,
    timerId,
    lastCallTime,
    lastInvokeTime = 0,
    leading = false,
    maxing = false,
    trailing = true;

  if (typeof func != "function") {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  wait = toNumber(wait) || 0;
  if (isObject(options)) {
    leading = !!options.leading;
    maxing = "maxWait" in options;
    maxWait = maxing
      ? nativeMax(toNumber(options.maxWait) || 0, wait)
      : maxWait;
    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) {
    lastInvokeTime = time;
    timerId = setTimeout(timerExpired, wait);
    return leading ? invokeFunc(time) : result;
  }

  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime,
      timeWaiting = wait - timeSinceLastCall;

    return maxing
      ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }

  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime;
    return (
      lastCallTime === undefined ||
      timeSinceLastCall >= wait ||
      timeSinceLastCall < 0 ||
      (maxing && timeSinceLastInvoke >= maxWait)
    );
  }

  function timerExpired() {
    var time = now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  function trailingEdge(time) {
    timerId = undefined;
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }

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

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

  function debounced() {
    var time = now(),
      isInvoking = shouldInvoke(time);

    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        clearTimeout(timerId);
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  return debounced;
}

// 節(jié)流
function throttle(func, wait, options) {
  var leading = true,
    trailing = true;

  if (typeof func != "function") {
    throw new TypeError(FUNC_ERROR_TEXT);
  }
  if (isObject(options)) {
    leading = "leading" in options ? !!options.leading : leading;
    trailing = "trailing" in options ? !!options.trailing : trailing;
  }
  return debounce(func, wait, {
    leading: leading,
    maxWait: wait,
    trailing: trailing,
  });
}

參考鏈接

lodash 的實(shí)現(xiàn)
可視化防抖與節(jié)流
防抖

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容