节流与防抖(Debouncing and Throttling)全面指北

1,516 阅读6分钟

什么是防抖(Debouncing)?

在 JavaScript 中,防抖debounce 函数可确保代码仅在每个用户输入时触发一次。搜索框建议、文本字段自动保存和消除双击都是防抖debounce 的用例。

防抖一词来自电子产品。当你按下一个按钮时,比方说在你的电视遥控器上,信号传输到遥控器的微芯片的速度非常快,以至于在你设法释放按钮之前,它会抖动,并且微芯片会多次记录你的“点击”。 debounce-button

为了缓解这种情况,一旦接收到来自按钮的信号,微芯片就会停止处理来自按钮的信号几微秒,在这期间你持续按下它也不会触发任何功能。

JavaScript中的防抖

在 JavaScript 中,用例是类似的。我们想触发一个函数,但每个用例只触发一次。用于确保耗时的任务不会频繁触发,从而导致网页性能停滞。 防抖debounce 函数将忽略对它的所有调用,直到调用停止指定时间段。只有这样它才会调用原始函数。

假设我们想要显示搜索查询的建议,但仅在访问者完成输入之后。或者我们想保存表单上的更改,但前提是用户没有积极处理这些更改时,因为每次“保存”都会访问数据库。

下面这个例子是防抖功能的简单实现:

function debounce(func, timeout = 300 ) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => {func.apply(this, args)}, timeout)
    };
}

function saveInput(){
    console.log('正在保存');
}

const change = debounce(() => saveInput());

防抖可以用在很多地方,比如:

//输入事件
<input type="text" onkeyup="Change()" />

//点击事件
<input type="text" onkeyup="Change()" />

//事件触发
window.addEventListener("scroll", Change);

debounce 是一个处理两个任务的特殊函数:

  • 给timer变量分配作用域
  • 指定的函数在特定时间触发

以文本输入为例,当用户写下第一个字母并释放键时,debounce首先使用 clearTimeout(timer) 重置计时器。此时,该步骤不是必需的,因为还没有任何计划。然后它安排提供的函数——saveInput()——在 300 毫秒后被调用。

但是假设用户一直在写,所以每次按键释放都会再次触发去抖动。每次调用都需要重置计时器,或者换句话说,使用 saveInput() 取消之前的计划,并重新安排它到一个新的时间——未来 300 毫秒。只要用户在 300 毫秒内不断敲击按键,这种情况就会持续下去。

当用户停止输入后, saveInput() 最终会被调用。

忽略后续事件

这对于触发自动保存或显示建议很有用。但是多次单击单个按钮的用例呢?我们不想等待最后一次点击,而是注册第一个并忽略其余的点击事件,看下面的代码:

function debounceLeading (func, timeout = 300) {
    let timer;
    return (...args) => {
        if(!timer) {
            func.apply(this, args);
        }
        clearTimeout(timer);
        timer = setTimeout(timer => {
            timer = undefined;
        },timeout);
    }

}

function saveInput(){
    console.log('正在保存');
}

const change = debounceLeading(() => saveInput());

这里我们在第一次按钮点击引起的第一次 debounceLeading 调用上触发 saveInput() 函数。我们将计时器销毁安排为 300 毫秒。在该时间范围内的每个后续按钮单击都已经定义了计时器,并且只会将销毁时间推迟 300 毫秒

防抖的库

如果你不想你的项目中使用你自己的 debounce 实现。广泛使用的 JS 库已经包含了它的实现。这里有一些例子:

//jQuery
$.debounce(300, saveInput);

//Lodash
_.debounce(saveInput, 300);

//UnderScore
_.debounce(saveInput, 300);

什么是节流(throttling)

节流或有时也称为节流功能是网站中使用的一种做法。限制函数意味着确保在指定的时间段内最多调用一次函数(例如每10秒一次)。这意味着如果某个函数“最近”运行,节流将阻止它运行。节流还确保函数以固定速率定期运行。

节流用于在每毫秒或特定时间间隔后调用一个函数,只有第一次单击会立即执行。

JavaScript中的节流

要了解节流,先从限制执行函数的速率开始:

const throttle = (func, limit) => {
    let inThrottle;
    return function() {
        const args = arguments;
        const context = this;
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit)
        }
    }
}

对函数的第一次调用将执行inThrottle设置限制期限。我们可以在此期间调用我们的函数,但在节流期过去之前它不会触发。过了节流期,下一次调用将触发并重复该过程,保证了在频繁触发函数的情况下让函数以固定的速率被调用

但是最后的调用呢?如果它在限制期内,它会被忽略,如果我们不想要它怎么办?我们需要抓住这个并在限制期之后执行它。

const throttle = (func, limit) => {
    let lastFunc, lastRan;
    return function () {
        const context = this;
        const args = arguments;
        if (!lastRan) {
            func.apply(context, args)
            lastRan = Date.now();
        }else{
            clearTimeout(lastFunc);
            lastFunc = setTimeout(function() {
                if ((Date.now() - lastRan) >= limit) {
                    func.apply(context, args);
                    lastRan = Date.now();
                }
            }, limit - (Date.now() - lastRan));
        }
    }
}

此实现确保我们捕获并执行最后一次调用。我们也在正确的时间调用它。通过创建一个变量lastRan 做到这一点,该变量是最后一次调用的时间戳。然后我们可以使用它来确定最后一次调用是否发生在节流限制内。我们还可以使用lastRan 确定节流函数是否已经运行。这使得前面的变量inThrottle 变得多余。

应用

游戏

在动作游戏中,用户通常通过按下按钮来执行关键动作(例如:射击、出拳)。但是,正如任何游戏玩家都知道的那样,用户经常按下按钮比必要的次数多得多,这可能是由于动作的兴奋和强度。所以用户可能会在 5 秒内击出 10 次“拳”,但游戏角色在一秒内只能出一拳。在这种情况下,限制动作是有意义的。在这种情况下,将“Punch”动作限制为一秒钟将忽略每秒按下的第二个按钮。

滚动事件处理

节流的另一个应用是在内容加载网页中,如用户不断滚动的 Facebook 和 Twitter。在这些情况下,如果滚动事件被触发得太频繁,可能会影响性能,因为它包含大量视频和图像。因此滚动事件必须使用节流。

总结

防抖和节流是相关的,它们都能提高网页的性能。只是它们应用于不同的情况:

  • 只关心最终状态时,使用防抖
  • 想要以受控的速率处理中间状态时,使用节流