丁香园 F2E

花厂设计招待所

debounce && throttle 简单实现


2011 年,Twitter 曝出一个 bug:当用户在滚动页面时,网站会变慢甚至无响应。John Resig 发表了一篇关于该问题的博客,并指出把高消耗的函数执行绑定在onscroll事件上是多么得不靠谱。下面以lodash中的debouncethrottle为例,来讲解函数节流在解决类似问题中的作用。

debounce

搜索引擎的自动补全功能已司空见惯,每当用户输入一个字符就去发一次请求,显然有点浪费。我们可以考虑在用户停止输入500 ms后再去请求,这样用户体验基本不会受到影响,也减少了不必要的请求,减轻了服务器的压力。

简单实现

function debounce(func, wait) {
  let timer;

  return function(...args) {
    const context = this;

    clearTimeout(timer);

    timer = setTimeout(function() {
      func.apply(context, args);
    }, wait);
  };
}

调用示例

/** 原来的做法 */
input.onkeypress = doSomeThing;

/** 使用函数节流 */
input.onkeypress = debounce(doSomeThing, 500);

/** 错误示例 */
input.onkeypress = function() {
  // 原因:返回一个函数,但没有执行
  // debounce(doSomeThing, 500);

  // 原因:每次事件触发单独创建一个闭包,会产生多个定时器
  // debounce(doSomeThing, 500)();
}

leading edge

如果用户打字速度很快,我们希望能在他输入第一个字符的时候就给出相关提示,可以使用leading参数来控制。

扩展leading参数

function debounce(func, wait, { leading = false } = {} ) {
  let context, xargs, timer;
  let firstInvoke = true;

  function invokeFunc() {
    func.apply(context, xargs);
  }

  function debounced(...args) {
    context = this;
    xargs = args;

    clearTimeout(timer);

    if (leading && firstInvoke) {
      invokeFunc();
      firstInvoke = false;
    }

    timer = setTimeout(function() {
      invokeFunc();
    }, wait);
  };

  return debounced;
}

maxWait

无限滚动在移动端场景中必不可少,我们希望能在页面滚动即将到达底部时去请求更多的数据。通过上面的实现,我们只有等用户停止滚动wait ms后才能开始检测到页面底部距离,未免有些慢了。不过我们通过maxWait参数,可以每隔maxWait ms就去执行检测代码来解决类似问题。

扩展maxWait参数

function debounce(func, wait, { leading = false, maxWait = 0 } = {}) {
  let context, xargs, timer, timeLast;
  let firstInvoke = true;

  function invokeFunc() {
    func.apply(context, xargs);
  }

  function debounced(...args) {
    context = this;
    xargs = args;

    const timeNow = +new Date();

    clearTimeout(timer);

    if (leading && firstInvoke) {
      invokeFunc();
      firstInvoke = false;
    }

    if (!timeLast) {
      timeLast = timeNow;
    }

    if (!!maxWait && timeNow - timeLast >= maxWait) {
      invokeFunc();
      timeLast = timeNow;
    } else {
      timer = setTimeout(function() {
        invokeFunc();
      }, wait);
    }
  };

  return debounced;
}

trailing edge

除了以上参数,debounce还提供了trailing参数。在调整浏览器窗口大小时会触发多次onresize事件,如果我们只对操作停止时的窗口尺寸感兴趣,那么就使用trailing = true来保证这一点(debouncetrailing默认为true)。

扩展trailing参数

function debounce(func, wait, { leading = false, maxWait = 0, trailing = true } = {}) {
  let context, xargs, timer, timeLast;
  let firstInvoke = true;

  function invokeFunc() {
    func.apply(context, xargs);
  }

  function debounced(...args) {
    context = this;
    xargs = args;

    const timeNow = +new Date();

    clearTimeout(timer);

    if (leading && firstInvoke) {
      firstInvoke = false;
      invokeFunc();
    }

    if (!timeLast) {
      timeLast = timeNow;
    }

    if (!!maxWait && timeNow - timeLast >= maxWait) {
      invokeFunc();
      timeLast = timeNow;
    } else if (trailing) {
      timer = setTimeout(function() {
        invokeFunc();
      }, wait);
    }
  };

  return debounced;
}

throttle

通过以上示例代码不难看出,使用debounce就可以实现throttle的功能,或者说throttle就是封装后的debounce。其实lodash的源码也是这么做得,underscore则将两个函数的实现分开了,有兴趣可以看一下

实现throttle

function throttle(func, wait, { leading = true, trailing = true } = {}) {
   return debounce(func, wait, { leading, maxWait: wait, trailing });
}

私有函数

除了以上参数,lodash中的debouncethrottle还包含以下两个私有函数可供调用,

  • cancel:取消延时函数(定时器)的执行

  • flush:立即执行用户回调

调用示例

const debounceFunc = _.debounce(doSomething, 500);

debounceFunc.cancel();
debounceFunc.flush();

应用场景

  • debounce

// 避免过分频繁得计算布局
window.onresize = debounce(calculateLayout, 150);

// 防止用户连续点击,发送重复请求
button.onclick = debounce(sendMail, 300, { leading: true, trailing: false });

// 恰当地处理批量登录
const debounceFunc = debounce(batchLog, 250, { maxWait: 1000 });
const source = new EventSource('/stream');

source.onmessage = debounceFunc;

// 取消节流调用
window.onpopstate = debounceFunc.cancel;
  • throttle

// 避免过分频繁得更新定位
window.onscroll = throttle(updatePosition, 100);

// 恰当地处理身份更新
const throttleFunc = throttle(renewToken, 300000, { 'trailing': false });

button.onclick = throttleFunc;

// 取消防抖调用
window.onpopstate = throttled.cancel;

参考

原创
下一篇:hybrid资源包增量更新机制: 优化与安全