All articles

Debouncing and Throttling

Nov 05, 2022

之前看过防抖节流但是忽略了一些细节的东西,比如防抖是从第一个触发事件开始还是从最后一个触发事件开始。

参考文章

函数防抖(debounce)

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。

在多次事件重复触发后,延迟一定的时间在执行这次事件。如下图,在事件触发 400ms 延迟之内如果再次触发,将会重置计时器,直到 400ms 内没有事件触发之后,再真正执行该事件方法。

debounce

当然也可以在开始触发事件的时候直接去执行,之后重复触发的事件不再执行,中间的间隔时间也是 400ms

Example of a “leading” debounce

例如:只要按下键盘,就会触发 Ajax 请求。不仅从资源上来说是很浪费的行为,而且实际应用中,用户也是输出完整的字符后,才会请求。加入了防抖以后,当频繁的输入时,并不会发送请求,只有在指定间隔内没有输入时,才会执行函数。如果停止输入但是在指定间隔内又输入,会重新触发计时。

以下是最简单的防抖函数,注意这里没有绑定 this 的指向。

const debounce = (fn, delay) => {
  let time;
  return (...args) => {
    if (time) {
      clearTimeout(time);
    }
    time = setTimeout(() => {
      fn(...args);
    }, delay);
  };
};

案例一:容器点击事件

案例二:输入框输入事件

函数节流(throttle)

规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

By using _.throttle, we don’t allow to our function to execute more than once every X milliseconds.

在节流方面,我们可以也可以通过定时器来实现节流。通过使用定时任务,延时方法执行。在延时的时间内,方法若被触发,则直接退出方法。从而,实现函数一段时间内只执行一次。

const throttle = (fn, delay) => {
  let timer;
  return (...args) => {
    // 如果有存在 timer 说明,还在这个区间内。
    if (timer) return;
    timer = setTimeout(() => {
      fn(...args);
      timer = null;
    }, delay);
  };
};

根据函数节流的原理,其实这里我们也可以不用 setTimeout 实现节流,可以直接通过闭包返回一个函数并且用到闭包函数外面的变量 last,两者原理是相同的。

const throttle = (fn, delay) => {
  let last = 0;
  return (...args) => {
    const now = Date.now();
    if (now - last < delay) {
      return;
    }
    last = now;
    return fn(...args);
  };
};

正如参考文章中所提到的,最常规的一个案例是,用户正在向下滚动您的无限滚动页面。需要检查用户离底部有多远,如果用户接近底部,需要通过 Ajax 请求更多内容并将其附加到页面。

案例一:滚动请求加载更多

Lodash 实现

Lodash 官方给出 debounce 比较完善,包含一下六个参数。具体实现:debounce

  1. func (Function): 要防抖动的函数。
  2. [wait=0] (number): 需要延迟的毫秒数。
  3. [options=] (Object): 选项对象。
    • [options.leading=false] (boolean): 指定在延迟开始前调用。
    • [options.maxWait] (number): 设置 func 允许被延迟的最大值。
    • [options.trailing=true] (boolean): 指定在延迟结束后调用。
const debounce = (fn, wait, options) => {
  // option: {leading: boolean, trailling: boolean, maxWait: number}
  // 上次调用的时间
  let lastCalltime;
  // 真实调用的时间
  let lastInvokeTime = 0;
  // 定时器ID
  let timerId;
  // 最大等待时间
  let maxWait;
  // 控制 maxWait 的标志
  let maxing = false;
  // 是否在头调用
  let leading = false;
  // 是否在尾调用
  let trailing = true;
  // 函数结果
  let result;
  // 所有参数
  let allArgs;

  if (options) {
    leading =
      "leading" in options
        ? !!options.leading
        : leading;
    trailing =
      "trailing" in options
        ? !!options.trailing
        : trailing;
    maxWait = options.maxWait;
    maxing = "maxWait" in options;
  }

  function debounced(...args) {
    const time = Date.now();
    const canInvoke = shouldInvoke(time);
    lastCalltime = time;
    allArgs = args;

    if (canInvoke) {
      // 起始调用 (第一次 或 一次调用完整结束后)
      if (timerId === undefined) {
        return leadingEdge(time);
      }
    }

    return result;
  }

  function shouldInvoke(time) {
    const timeSinceLastCall = time - lastCalltime;
    const timeSinceLastInvoke =
      time - lastInvokeTime;
    // 1. 第一次调用
    // 2. 间隔时间大于wait
    // 3. 等待时间大于了maxWait
    return (
      timerId === undefined ||
      timeSinceLastCall >= wait ||
      (maxing && timeSinceLastInvoke >= maxWait)
    );
  }

  function leadingEdge(time) {
    lastInvokeTime = time;

    // 调用的开始,等待完整的wait
    timerId = setTimeout(timerExpired, wait);

    if (leading) {
      // 处理leading
      return invokeFunc(time);
    }

    return result;
  }

  function trailingEdge(time) {
    // 运行trailling意味着一次调用的结束
    timerId = undefined;

    if (trailing) {
      return invokeFunc(time);
    }

    return result;
  }

  function timerExpired() {
    const time = Date.now();
    // 再次检查是否可以调用
    // 多次点击会改变lastCallTime, 所以shouldInvoke返回值也会改变
    if (shouldInvoke(time)) {
      // 处理trailling
      return trailingEdge(time);
    }
    // 不满足条件,继续等待
    // remainingWait来计算需要等待的具体时间
    timerId = setTimeout(
      timerExpired,
      remainingWait(time)
    );
  }

  function invokeFunc(time) {
    lastInvokeTime = time;
    const argsCopy = allArgs;
    allArgs = undefined;
    if (allArgs) {
      return fn(...argsCopy);
    }
    return fn();
  }

  function remainingWait(time) {
    const timeSinceLastCall = time - lastCalltime;
    const timeSinceLastInvoke =
      time - lastInvokeTime;

    const timeToWait = wait - timeSinceLastCall;

    // maxing模式,返回 正常需要等待的时间 和 距离maxWait剩余时间 的小值
    return maxing
      ? Math.min(
          timeToWait,
          maxWait - timeSinceLastInvoke
        )
      : timeToWait;
  }

  return debounced;
};

总结

debounce

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

throttle

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

antcao.me © 2022-PRESENT

: 0x9aB9C...7ee7d