节流 debounce 和 防抖 throttle 的简单实现

发布于 2024-02-15 08:19:02 字数 6246 浏览 17 评论 0

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;

参考

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

Spring初心

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

qq_E2Iff7

文章 0 评论 0

Archangel

文章 0 评论 0

freedog

文章 0 评论 0

Hunk

文章 0 评论 0

18819270189

文章 0 评论 0

wenkai

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文