debounce

"Use only that which works, and take it from any place you can find it." —Bruce Lee

本文为 《lodash 源码阅读》 系列文章,后续内容会在 github 中发布,欢迎 star,gitbook 同步更新。

依赖

import isObject from './isObject.js';
import root from './.internal/root.js'; // 当前运行环境

源码注释

/**
 * 创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。
 * debounced(防抖动)函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。
 * 可以提供一个 options(选项) 对象决定如何调用 func 方法,options.leading 与|或 options.trailing 决定延迟前后如何触发。
 * func 调用时会传入最后一次提供给 debounced(防抖动)函数 的参数。
 * 后续调用的 debounced(防抖动)函数返回是最后一次 func 调用的结果。
 *
 * 注意: 如果 leading 和 trailing 选项为 true, 则 func 允许 trailing 方式调用的条件为: 在 wait 期间多次调用防抖方法。
 *
 * 如果 wait 为 0 并且 leading 为 false, func调用将被推迟到下一个点,类似setTimeout为0的超时。
 *
 * See [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/)
 * for details over the differences between `debounce` and `throttle`.
 *
 * @since 0.1.0
 * @category Function
 * @param {Function} func 要防抖动的函数。
 * @param {number} [wait=0]
 *  需要延迟的毫秒数,如果为空,间隔则为 `requestAnimationFrame`。
 * @param {Object} [options={}] 选项对象。
 * @param {boolean} [options.leading=false]
 *  指定在延迟开始前调用。
 * @param {number} [options.maxWait]
 *  设置 func 允许被延迟的最大值。
 * @param {boolean} [options.trailing=true]
 *  指定在延迟结束后调用。
 * @returns {Function} 返回新的 debounced(防抖动)函数。
 */
function debounce(func, wait, options) {
  let lastArgs, // 最后一次 debounced 返回函数执行时的参数 args
    lastThis, // 最后一次 debounced 返回函数执行时的上下文 this
    maxWait, // func 允许被延迟的最大值。
    result, // 最后一次执行的结果
    timerId, // 定时器id
    lastCallTime; // 最后一次 debounced 返回函数执行时的时间

  let lastInvokeTime = 0; // 最后一次 func 被执行的时间
  let leading = false; // 是否在延迟开始前调用
  let maxing = false; // 是否存在 func 允许被延迟的最大值 maxWait
  let trailing = true; // 是否在延迟结束后调用

  // 通过显示传入 wait=0 绕过 requestAnimationFrame 的使用
  const useRAF = !wait && wait !== 0 && typeof root.requestAnimationFrame === 'function';

  // func 类型判断
  if (typeof func !== 'function') {
    throw new TypeError('Expected a function');
  }

  wait = +wait || 0;

  // 获取配置属性
  if (isObject(options)) {
    leading = !!options.leading;
    maxing = 'maxWait' in options;
    maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;
    trailing = 'trailing' in options ? !!options.trailing : trailing;
  }

  /**
   * 调用 func 的函数
   * @param {Number} time 时间戳
   */
  function invokeFunc(time) {
    // 获取最后一次执行时的 参数 args 及 上下文 this
    const args = lastArgs;
    const thisArg = lastThis;

    // 清空缓存
    lastArgs = lastThis = undefined;

    // 记录最后一次执行的时间
    lastInvokeTime = time;

    // 执行 func
    result = func.apply(thisArg, args);

    // 返回运行结果
    return result;
  }

  /**
   * 启动计时器
   * @param {Function} pendingFunc 待执行的函数
   * @param {Number} wait 延迟时间
   */
  function startTimer(pendingFunc, wait) {
    // 如果需要使用 requestAnimationFrame,则使用 requestAnimationFrame 进行延时执行
    if (useRAF) {
      root.cancelAnimationFrame(timerId);
      return root.requestAnimationFrame(pendingFunc);
    }
    // 否则使用 setTimeout 延时 wait ms 执行
    return setTimeout(pendingFunc, wait);
  }

  // 取消定时器
  function cancelTimer(id) {
    if (useRAF) {
      return root.cancelAnimationFrame(id);
    }
    clearTimeout(id);
  }

  // 延迟 leading 边缘检测
  function leadingEdge(time) {
    // 重置最后一次执行时间,达到重置 `maxWait` 功能效果
    lastInvokeTime = time;
    // 执行定时器,检测 trailing 后置操作是否需要被执行
    timerId = startTimer(timerExpired, wait);
    // 执行 leading 前置执行
    return leading ? invokeFunc(time) : result;
  }

  // 计算剩余等待时间
  function remainingWait(time) {
    // 距离 debounced 最后一次被调用的时间
    const timeSinceLastCall = time - lastCallTime;
    // 距离 func 最后一次执行的时间
    const timeSinceLastInvoke = time - lastInvokeTime;
    // 剩余等待时间
    const timeWaiting = wait - timeSinceLastCall;

    // 根据 maxWait 情况,返回剩余等待时间
    return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
  }

  // 判断是否需要执行 func
  function shouldInvoke(time) {
    // 距离 debounced 最后一次被调用的时间
    const timeSinceLastCall = time - lastCallTime;
    // 距离 func 最后一次执行的时间
    const timeSinceLastInvoke = time - lastInvokeTime;

    // 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 === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait);
  }

  function timerExpired() {
    const time = Date.now();
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // 重置定时器
    timerId = startTimer(timerExpired, remainingWait(time));
  }

  // 延迟 trailing 边缘检测
  function trailingEdge(time) {
    timerId = undefined;

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

  // 取消 debounced,重置所有参数
  function cancel() {
    if (timerId !== undefined) {
      cancelTimer(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

  // flush 立即执行 func
  function flush() {
    return timerId === undefined ? result : trailingEdge(Date.now());
  }

  // 判断是否在定时器倒计时 pending 过程
  function pending() {
    return timerId !== undefined;
  }

  function debounced(...args) {
    const time = Date.now();
    const isInvoking = shouldInvoke(time);

    lastArgs = args;
    lastThis = this;
    lastCallTime = time;

    if (isInvoking) {
      if (timerId === undefined) {
        return leadingEdge(lastCallTime);
      }
      if (maxing) {
        // 存在 maxing 情况,重置定时器并立即执行
        timerId = startTimer(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
    }
    if (timerId === undefined) {
      timerId = startTimer(timerExpired, wait);
    }
    return result;
  }
  debounced.cancel = cancel;
  debounced.flush = flush;
  debounced.pending = pending;
  return debounced;
}

相关链接

Last updated

Was this helpful?