debounce && throttle 简单实现

回到首页

DXY Tech&Product Division


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;

参考

分类

TOP