js中的简单节流阀


72

我正在寻找JS中的简单节流阀。我知道像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秒后仍会触发第二次按键。我不要这种行为。


3
1. jQuery有一个插件benalman.com/projects/jquery-throttle-debounce-plugin 2.为什么不只使用下划线/ lodash的节流实现?
Oleg 2014年

@Oleg是否可以仅使用节流阀而不导入整个库?
Mia 2014年

1
您可以设置一个示例,还是至少更好地解释用例?通常,按键油门的设置非常简单,就像这样-> jsfiddle.net/a3w6pLbj/1
adeneo 2014年

Answers:


89

我将使用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的注释


46
在我看来并不简单。这是简单的一个很好的例子
vsync

9
确实,这并不简单。但这是生产就绪且开源的。
克莱门特PREVOST

11
之所以不像链接到@vsync那样简单,原因之一是因为它支持尾随调用。对于此函数,如果您两次调用结果函数,则将导致对包装函数的两次调用:一次调用,一次在延迟之后。在与之链接的一个vsync中,它将导致单个立即调用,但在延迟后没有响应。在许多情况下,接收尾随调用对于获取最后的视口大小或您要执行的操作非常重要。
亚罗尼厄斯'16

3
请永远不要使用它。我无意自大。相反,我只是想实用。这个答案比需要的复杂得多。我为这个问题发布了一个单独的答案,该答案可以用更少的代码行来完成所有这些工作。
杰克·吉芬

2
@Nico的arguments:对象的任何函数不是箭头函数内部总是被定义developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/...
克莱门特普雷沃

13

callback:采用应调用的函数

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

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

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

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);


6
@lolzerywowzery好像您的答案没有需要的那么简单
Denny

1
我建议像这样触发回调:callback(...arguments)保留原始参数。非常方便
vsync

这应该是公认的答案。简单容易。
Gaurang Tandon

@Denny这个答案束缚了巨大的浏览器资源。它会在创建的每个处理程序中启动一个全新的Intervalling函数。即使删除了事件侦听器,连续轮询也会耗尽计算机资源,从而导致高内存使用率,冻结和分页砖。
杰克·吉芬

4
请不要在生产代码中使用此答案。这是不良编程的极好选择。假设您的页面上有1000个按钮(听起来可能很多,但请三思而后行:按钮隐藏在任何地方:在弹出窗口,子菜单,面板等中),并希望每个按钮最多每200秒触发一次。它们很可能同时在每333毫秒(或每秒3次)中同时启动,因此所有这些计时器都需要再次检查时,会出现巨大的延迟尖峰。此答案完全setInterval是出于原本不想要的目的而滥用的。
杰克·吉芬

12

那这个呢?

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

简单。

您可能有兴趣查看源代码


1
这是页面上最干净的最小实现。
劳伦斯·多尔

对我来说,这仅适用于Date.now()而不是new Date()
Ian Jones

11

添加到这里的讨论(以及更近的游客),如果不使用的原因,几乎实际上throttlelodash是有一个小尺寸的包或捆绑,则有可能只包括throttle在你的包,而不是整个lodash库。例如在ES6中,它将类似于:

import throttle from 'lodash/throttle';

此外,还有一个throttle只能从封装lodash称为lodash.throttle其可以用简单的使用import在ES6或require在ES5。


4
检查代码。它使用2个文件导入,因此这意味着您需要3个文件才能使用简单的节流功能。我要说的有点过大,特别是如果某人(例如我自己)需要约200行代码程序的调节功能时。
vsync

6
是的,它在内部使用 debounce,并isObject,整束的大小来围绕2.1KB精缩。我想,对于一个小程序没有意义,但是我更喜欢在较大的项目中使用它,而不是创建自己的油门功能,我也必须对其进行测试:)
Divyanshu Maithani

6

我只需要一个用于窗口大小调整事件的油门/反跳功能,并且很好奇,我还想知道它们是什么以及它们如何工作。

我已经阅读了多篇关于SO的博客文章和QA,但它们似乎都使这一过程变得过于复杂,建议使用库,或者只是提供了说明,而不是简单的普通JS实现。

由于内容丰富,因此我将不提供描述。所以这是我的实现:

function throttle(callback, delay) {
    var timeoutHandler = null;
    return function () {
        if (timeoutHandler == null) {
            timeoutHandler = setTimeout(function () {
                callback();
                clearInterval(timeoutHandler);
                timeoutHandler = null;
            }, delay);
        }
    }
}

function debounce(callback, delay) {
    var timeoutHandler = null;
    return function () {
        clearTimeout(timeoutHandler);
        timeoutHandler = setTimeout(function () {
            callback();
        }, delay);
    }
}

这些可能需要调整(例如,最初不立即调用回调)。

查看操作上的差异(尝试调整窗口大小):

JSFiddle


设计不当会导致其所附加内容的大量延迟,从而使网站对用户无响应。
杰克·吉芬

1
@commonSenseCode“甚至不起作用”甚至是什么意思?我提供了演示代码。显然有效。请尝试更加详细。不管什么都行不通,我很确定这与您的实现有关。
akinuri

5

这是我在9LOC的ES6中实现油门功能的方式,希望对您有所帮助

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

单击此链接以查看其工作方式。


2
简单,但效果不佳:即使不适当,它也会延迟功能,并且不会使待处理事件保持最新状态,从而可能导致用户交互滞后的重大滞后。同样,使用...扩展语法也是不合适的,因为只有一个参数传递给事件侦听器:事件对象。
杰克·吉芬

1
@JackGiffin:使用传播并不恰当;没有任何限制将油门功能限制为仅用于事件处理程序。
劳伦斯·多尔

1

我制作了一个具有一些限制功能的npm软件包:

npm install function-throtler

节流和队列

返回最多每个W毫秒调用的函数版本,其中W等待。对函数的调用比W排队的频率要高得多,每W ms排队调用一次

节制更新

返回最多每个W毫秒调用的函数版本,其中W等待。对于比W多发生的呼叫,最后一次呼叫将是被呼叫的呼叫(最后优先)

风门

限制您的函数最多每W毫秒调用一次,其中W等待。通过W的呼叫被丢弃


1

有一个适合此目的的库,它是Ember的Backburner.js。

https://github.com/BackburnerJS/

您会这​​样使用。

var backburner = new Backburner(["task"]); //You need a name for your tasks

function saySomething(words) {
  backburner.throttle("task", console.log.bind(console, words)
  }, 1000);
}


function mainTask() {
  "This will be said with a throttle of 1 second per word!".split(' ').map(saySomething);
}

backburner.run(mainTask)

1

该节流功能基于ES6。回调函数接受参数(args),但仍与节流函数一起包装。可以根据您的应用需求自由定制延迟时间。每100ms 1次用于开发模式,事件“ oninput”仅是其频繁使用的一个示例:

const callback = (...args) => {
  console.count('callback throttled with arguments:', args);
};

throttle = (callback, limit) => {
  let timeoutHandler = 'null'

  return (...args) => {
    if (timeoutHandler === 'null') {
      timeoutHandler = setTimeout(() => {            
        callback(...args)
        timeoutHandler = 'null'
      }, limit)
    }
  }
}

window.addEventListener('oninput', throttle(callback, 100));

PS如@Anshul所解释:节流强制函数在一段时间内可以被调用的最大次数。如“每100毫秒最多执行一次此功能”。


在调用回调之前,它还要等待1000毫秒,这一点都不好。用户需要响应式页面,而不是缓慢的噩梦。
杰克·吉芬

感谢您的评论。您可以根据应用程序的要求自定义回调时间。通常在100到500毫秒之间。这1000毫秒使您可以检查和调试功能。
罗马

@JackGiffin在某些情况下,不需要前沿调用。自动保存就是一个例子。
小资

@pettys如果没有前沿调用,则它不是节流功能。而是,它是一个防抖动功能。
杰克·吉芬

4
@JackGiffin我不认为这是油门和反跳之间的正确区别。我相信节流是“不超过x次/秒调用”,而反跳是“对于间隔小于x秒的源事件序列,请将整个序列视为单个实例。” 细微的差别,但是在这里有很好的说明:demo.nimius.net/debounce_throttle 在我看来,节流和去抖动都具有有意义且有用的无前沿配置。
小资

1

在下面的示例中,尝试多次单击该按钮,但是该myFunc功能仅在3秒钟内执行一次。该函数throttle与要执行的函数和延迟一起传递。它返回一个闭包,存储在中obj.throttleFunc。现在,由于obj.throttleFunc存储了一个闭包,因此isRunning将在其中保留其值。

function throttle(func, delay) {
  let isRunning;
  return function(...args) {
    let context = this;        // store the context of the object that owns this function
    if(!isRunning) {
      isRunning = true;
      func.apply(context,args) // execute the function with the context of the object that owns it
      setTimeout(function() {
        isRunning = false;
      }, delay);
    }
  }
}

function myFunc(param) {
  console.log(`Called ${this.name} at ${param}th second`);
}

let obj = {
  name: "THROTTLED FUNCTION ",
  throttleFunc: throttle(myFunc, 3000)
}

function handleClick() {
  obj.throttleFunc(new Date().getSeconds());
}
button {
  width: 100px;
  height: 50px;
  font-size: 20px;
}
    <button onclick="handleClick()">Click me</button>


如果我们不希望传递上下文或参数,则其更简单的版本如下:

function throttle(func, delay) {
  let isRunning;
  return function() {
    if(!isRunning) {
      isRunning = true;
      func()
      setTimeout(function() {
        isRunning = false;
      }, delay);
    }
  }
}

function myFunc() {
  console.log('Called');
}


let throttleFunc = throttle(myFunc, 3000);

function handleClick() {
  throttleFunc();
}
button {
  width: 100px;
  height: 50px;
  font-size: 20px;
}
<button onclick="handleClick()">Click me</button>


1
function throttle(targetFunc, delay){
  let lastFunc;
  let lastTime;

  return function(){
    const _this = this;
    const args = arguments;

    if(!lastTime){
      targetFunc.apply(_this, args);
      lastTime = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function(){
        targetFunc.apply(_this, args);
        lastTime = Date.now();
      }, delay - (Date.now() - lastTime));
    }
  }
}

试试看 :

window.addEventListener('resize', throttle(function() {
  console.log('resize!!');
}, 200));

这是我见过的最好的简单实现,但是实际上您可以使它稍微简单一些,将在下面添加新答案
rsimp

0

下面是我能想到的最简单的节气门,它处于13 LOC。每次调用该函数都会创建一个超时,并取消旧的超时。如预期的那样,使用适当的上下文和参数调用原始函数。

function throttle(fn, delay) {
  var timeout = null;

  return function throttledFn() {
    window.clearTimeout(timeout);
    var ctx = this;
    var args = Array.prototype.slice.call(arguments);

    timeout = window.setTimeout(function callThrottledFn() {
      fn.apply(ctx, args);
    }, delay);
  }
}

// try it out!
window.addEventListener('resize', throttle(function() {
  console.log('resize!!');
}, 200));


11
这是一个反跳,而不是节流阀。如果我在1000毫秒内将其称为100,它将在1200毫秒后触发1次。节流功能应触发5次
-Dogoku

0

这是我自己的Vikas发布版本:

throttle: function (callback, limit, time) {
    var calledCount = 0;
    var timeout = null;

    return function () {
        if (limit > calledCount) {
            calledCount++;
            callback(); 
        }
        if (!timeout) {
            timeout = setTimeout(function () {
                calledCount = 0
                timeout = null;
            }, time);
        }
    };
}

我发现使用setInterval不是一个好主意。


0

我还想提出一个简单的解决方案,以解决只有一个您知道要调用的函数的情况(例如:搜索)

这是我在项目中所做的

let throttle;

function search() {
    if (throttle) {
      clearTimeout(throttle);
    }
    throttle = setTimeout(() => {
      sendSearchReq(str)
    }, 500);
  }

在输入更改事件上调用搜索


1
这不完全是节气门功能。每次调用该search()函数都会重置超时。因此,如果我search()每毫秒调用一次该函数,它将仅执行sendSearchReq一次,然后再执行一次,而不是每500毫秒执行一次。该功能更多的是延迟,而不是节流阀。
耕田机

这不是防弹跳
爱德华·杰克

0

简单的油门功能-

注意-继续单击按钮,您将在第一次单击时看到控制台日志,然后每隔5秒钟才看到控制台日志,直到您继续单击为止。

HTML-

<button id='myid'>Click me</button>

Javascript-

const throttle = (fn, delay) => {
  let lastTime = 0;
  return (...args) => {
      const currentTime = new Date().getTime();
      if((currentTime - lastTime) < delay) {
        return;
      };
      lastTime = currentTime;
      return fn(...args);
  }
};

document.getElementById('myid').addEventListener('click', throttle((e) => {
  console.log('I am clicked');
}, 5000));

0

我们还可以使用标志实现

var expensive = function(){
    console.log("expensive functionnns");
}

window.addEventListener("resize", throttle(expensive, 500))

function throttle(expensiveFun, limit){
    let flag = true;
    return function(){
        let context = this;
        let args = arguments;
        if(flag){
            expensiveFun.apply(context, args);
            flag = false;
            setTimeout(function(){
                flag = true;
            }, limit);
        }
    }
}


0

我在这里看到了很多答案,这些答案对于“一个简单的js节气门”来说太复杂了。

几乎所有更简单的答案都只是忽略“节流”中的调用,而不是将执行延迟到下一个时间间隔。

这是一个简单的实现,还可以处理“节流”调用:

const throttle = (func, limit) => {
  let lastFunc;
  let lastRan = Date.now() - (limit + 1); //enforces a negative value on first run
  return function(...args) {
    const context = this;
    clearTimeout(lastFunc);
    lastFunc = setTimeout(() => {
      func.apply(context, args);
      lastRan = Date.now();
    }, limit - (Date.now() - lastRan)); //negative values execute immediately
  }
}

对于简单的去抖动,这几乎是完全相同的实现。它只是添加了超时延迟的计算,该计算需要跟踪上次运行该函数的时间。见下文:

const debounce = (func, limit) => {
  let lastFunc;
  return function(...args) {
    const context = this;
    clearTimeout(lastFunc);
    lastFunc = setTimeout(() => {
      func.apply(context, args)
    }, limit); //no calc here, just use limit
  }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.