JavaScript 中的简单节流阀

IT技术 javascript jquery throttling
2021-01-16 17:06:49

我正在寻找一个简单的 JavaScript 节流阀。我知道像 lodash 和 underscore 这样的库有它,但仅对于一个函数来说,包含任何这些库都是多余的。

我也在检查 jQuery 是否有类似的功能 - 找不到。

我找到了一个工作油门,这是代码:

function throttle(fn, threshhold, scope) {
  threshhold || (threshhold = 250);
  var last,
      deferTimer;
  return function () {
    var context = scope || this;

    var now = +new Date,
        args = arguments;
    if (last && now < last + threshhold) {
      // hold on to it
      clearTimeout(deferTimer);
      deferTimer = setTimeout(function () {
        last = now;
        fn.apply(context, args);
      }, threshhold);
    } else {
      last = now;
      fn.apply(context, args);
    }
  };
}

这样做的问题是:它在油门时间完成后再次触发该功能。所以让我们假设我做了一个油门,每 10 秒触发一次按键 - 如果我按键 2 次,它仍然会在 10 秒完成后触发第二次按键。我不想要这种行为。

6个回答

我会使用underscore.jslodash源代码来找到这个函数的经过良好测试的版本。

这是下划线代码的略微修改版本,用于删除对 underscore.js 本身的所有引用:

// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
function throttle(func, wait, options) {
  var context, args, result;
  var timeout = null;
  var previous = 0;
  if (!options) options = {};
  var later = function() {
    previous = options.leading === false ? 0 : Date.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    var now = Date.now();
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
    return result;
  };
};

请注意,如果您不需要所有强调支持的选项,则可以简化此代码。

请在下面找到此功能的一个非常简单且不可配置的版本:

function throttle (callback, limit) {
    var waiting = false;                      // Initially, we're not waiting
    return function () {                      // We return a throttled function
        if (!waiting) {                       // If we're not waiting
            callback.apply(this, arguments);  // Execute users function
            waiting = true;                   // Prevent future invocations
            setTimeout(function () {          // After a period of time
                waiting = false;              // And allow future invocations
            }, limit);
        }
    }
}

编辑 1:删除了对下划线的另一个引用,感谢@Zettam 的评论

编辑 2:添加了关于 lodash 和可能的代码简化的建议,感谢 @lolzery @wowzery 的评论

编辑 3:由于流行的请求,我添加了一个非常简单的、不可配置的函数版本,改编自 @vsync 的评论

请永远不要使用它。我无意傲慢。相反,我只是想实用一些。这个答案比它需要的要复杂得多。我针对这个问题发布了一个单独的答案,它以更少的代码行完成了所有这些以及更多的工作。
2021-03-18 17:06:49
这不像链接到的@vsync 那样简单的原因之一是因为它支持尾随调用。有了这个,如果你两次调用结果函数,它将导致对包装函数的两次调用:一次是立即调用,一次是在延迟之后。在链接到的一个 vsync 中,它将导致一个单一的、立即的调用,但在延迟之后不会调用。在许多情况下,接收尾随调用对于获取最后一个视口大小或您想要执行的任何操作非常重要。
2021-03-25 17:06:49
@Nicoarguments对象总是在任何不是箭头函数的函数中定义:developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
2021-03-27 17:06:49
对我来说看起来并不简单。这是一个简单好例子
2021-03-30 17:06:49
的确,这并不简单。但它已准备好投入生产并且是开源的。
2021-04-05 17:06:49

那这个呢?

function throttle(func, timeFrame) {
  var lastTime = 0;
  return function () {
      var now = new Date();
      if (now - lastTime >= timeFrame) {
          func();
          lastTime = now;
      }
  };
}

简单的。

您可能有兴趣查看源代码

对我来说,这只适用于Date.now()而不是new Date()
2021-03-21 17:06:49
这是页面上最干净的最小实现。
2021-03-31 17:06:49
now - lastTime因为now是日期,所以我在 TS 中也有警告替换Date.now()为获得一个数字似乎是合法的。
2021-04-02 17:06:49
与使用相比,这种方法有setTimeout什么缺点吗?
2021-04-05 17:06:49
@Vic 不能保证最后一次调用您的函数
2021-04-08 17:06:49

添加到此处的讨论中(以及对于最近的访问者),如果不使用几乎事实上的throttlefrom 的原因lodash是具有较小尺寸的包或包,则可以仅包含throttle在您的包中而不是整个lodash库中。例如在 ES6 中,它会是这样的:

import throttle from 'lodash/throttle';

此外,throttle只有一个来自lodashcalled 的lodash.throttle可以与importES6 或requireES5 中的 simple 一起使用

检查了代码。它使用 2 个文件导入,因此这意味着您将需要 3 个文件来实现简单的节流功能。我会说有点矫枉过正,特别是如果有人(像我自己)需要一个约 200 行代码程序的节流功能。
2021-03-13 17:06:49
喜欢lodash-eslodash现代项目
2021-03-18 17:06:49
是的,它在内部使用debounceisObject,整个包大小缩小到大约2.1KB我想,对于一个小程序没有意义,但我更喜欢在更大的项目中使用它而不是创建我自己的油门功能,我也必须测试:)
2021-03-19 17:06:49

callback : 接受应该被调用的函数

limit : 在时间限制内应调用该函数的次数

time : 重置限制计数的时间跨度

功能和用法:假设您有一个允许用户在 1 分钟内调用 10 次的 API

function throttling(callback, limit, time) {
    /// monitor the count
    var calledCount = 0;

    /// refresh the `calledCount` varialbe after the `time` has been passed
    setInterval(function(){ calledCount = 0 }, time);

    /// creating a closure that will be called
    return function(){
        /// checking the limit (if limit is exceeded then do not call the passed function
        if (limit > calledCount) {
            /// increase the count
            calledCount++;
            callback(); /// call the function
        } 
        else console.log('not calling because the limit has exceeded');
    };
}
    
//////////////////////////////////////////////////////////// 
// how to use

/// creating a function to pass in the throttling function 
function cb(){
    console.log("called");
}

/// calling the closure function in every 100 milliseconds
setInterval(throttling(cb, 3, 1000), 100);

请不要在生产代码中使用这个答案。这是糟糕的编程的非常糟糕的表现。假设您的页面上有 1000 个按钮(听起来可能很多,但再想一想:按钮隐藏在任何地方:在弹出窗口、子菜单、面板等中)并希望限制每个按钮最多每 200 秒触发一次 现在,因为它们可能会同时启动,每 333 毫秒(或每秒 3 次),当所有这些计时器都需要再次检查时,会有一个巨大的延迟峰值。这个答案完全滥用setInterval了它不打算做的目的。
2021-03-15 17:06:49
@lolzerywowzery 你的答案并不像它需要的那么复杂
2021-03-25 17:06:49
我建议像这样触发回调:callback(...arguments)保留原始参数。非常便利
2021-03-29 17:06:49
这应该是公认的答案。简单易行。
2021-03-30 17:06:49
@Denny 这个答案占用了大量的浏览器资源。它会在它创建的每个处理程序中启动一个全新的 Intervalling 函数。即使在您移除事件侦听器之后,连续轮询也会耗尽计算机资源,导致内存使用率过高、冻结和页面砖块。
2021-04-10 17:06:49

下面是我在 9LOC 的 ES6 中实现油门功能的方法,希望对你有帮助

function throttle(func, delay) {
  let timeout = null
  return function(...args) {
    if (!timeout) {
      timeout = setTimeout(() => {
        func.call(this, ...args)
        timeout = null
      }, delay)
    }
  }
}

单击此链接以查看它是如何工作的。

这将触发...args初始调用的 ,而不是最新调用的。
2021-03-20 17:06:49
@JackGiffin :使用传播并不少见;没有任何限制油门功能仅用于事件处理程序。
2021-03-26 17:06:49
简单,但相当无效:即使在不合适的情况下,它也会延迟功能,并且不会使待处理的事件保持新鲜,从而导致用户交互滞后的潜在重大滞后。此外,使用...传播语法是不合适的,因为只有 1 个参数被传递给事件侦听器:事件对象。
2021-04-03 17:06:49