Simple throttle in JavaScript

I am looking for a simple throttle in JavaScript. I know libraries like lodash and underscore have it, but only for one function it will be overkill to include any of those libraries.

I was also checking if jQuery has a similar function - could not find.

I have found one working throttle, and here is the code:

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

The problem with this is: it fires the function once more after the throttle time is complete. So let's assume I made a throttle that fires every 10 seconds on keypress - if I do keypress 2 times, it will still fire the second keypress when 10 seconds are completed. I do not want this behavior.

134817 次浏览

我将使用 下划线 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);
}
}
}

Edit 1: Removed another reference to underscore, thx to @Zettam 's comment

编辑2: 添加了关于 loash 和可能的代码简化的建议,thx 至@lolzery@wowzery 的注释

Edit 3: Due to popular requests, I added a very simple, non-configurable version of the function, adapted from @vsync 's comment

下面是我能想到的最简单的油门,在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));

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

如果不使用来自 lodash的几乎事实上的 throttle的原因是为了拥有一个更小的包或包,那么在您的包中可能只包含 throttle,而不是整个 lodash库。例如在 ES6中,类似于:

import throttle from 'lodash/throttle';

此外,还有一个来自 lodash称为 lodash.throttle的仅 throttle软件包,可以与 ES6中的简单 import或 ES5中的 require一起使用。

下面是我如何在9LOC 中在 ES6中实现油门功能,希望对您有所帮助

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

点击这个 链接看看它是如何工作的。

I made a npm package with some throttling functions:

Npm 安装 功能节流阀

加速排队

返回函数的一个版本,该版本最多可以每 W 毫秒调用一次,其中 W 等待。对函数的调用比 W 发生得更频繁,会排队等待每个 W ms 调用

节流更新

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

油门

限制你的函数最多每 W 毫秒被调用一次,在这里 W 是等待的。对 W 的调用会被丢弃

有一个图书馆适合这个目的,它是来自 Ember 的 Backburner.js。

Https://github.com/backburnerjs/

You'd use it so.

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)

我只是需要一个节流/退出窗口大小调整事件的函数,出于好奇,我也想知道这些是什么,以及它们是如何工作的。

我读过很多关于 SO 的博客文章和 QA,但是它们似乎都过于复杂,建议使用库,或者仅仅提供描述,而不是简单的 JS 实现。

I won't provide a description since it's plentiful. So here's my implementation:

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


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

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

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

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


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


var cellDefault  = document.querySelector("#cellDefault div");
var cellThrottle = document.querySelector("#cellThrottle div");
var cellDebounce = document.querySelector("#cellDebounce div");


window.addEventListener("resize", function () {
var span = document.createElement("span");
span.innerText = window.innerWidth;
cellDefault.appendChild(span);
cellDefault.scrollTop = cellDefault.scrollHeight;
});


window.addEventListener("resize", throttle(function () {
var span = document.createElement("span");
span.innerText = window.innerWidth;
cellThrottle.appendChild(span);
cellThrottle.scrollTop = cellThrottle.scrollHeight;
}, 500));


window.addEventListener("resize", debounce(function () {
var span = document.createElement("span");
span.innerText = window.innerWidth;
cellDebounce.appendChild(span);
cellDebounce.scrollTop = cellDebounce.scrollHeight;
}, 500));
table {
border-collapse: collapse;
margin: 10px;
}
table td {
border: 1px solid silver;
padding: 5px;
}
table tr:last-child td div {
width: 60px;
height: 200px;
overflow: auto;
}
table tr:last-child td span {
display: block;
}
<table>
<tr>
<td>default</td>
<td>throttle</td>
<td>debounce</td>
</tr>
<tr>
<td id="cellDefault">
<div></div>
</td>
<td id="cellThrottle">
<div></div>
</td>
<td id="cellDebounce">
<div></div>
</td>
</tr>
</table>

JSFiddle

下面是我自己的 维卡斯文章:

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不是一个好主意。

这个节流功能是建立在 ES6之上的。回调函数接受参数(args) ,但它仍然使用油门函数封装。可以根据您的应用程序需要自定义延迟时间。开发模式使用每100毫秒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));

附言。正如@Anshul 解释的那样: 随着时间的推移,调节强制执行一个函数可以被调用的最大次数。比如“每100毫秒最多执行一次这个函数”

在下面的例子中,尝试多次点击按钮,但是 myFunc函数只会在3秒内执行一次。 The function throttle is passed with the function to be executed and the delay.It returns a closure, which is stored in 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>


If we don't want the context or arguments to be passed, then a simpler version of this would be as following:

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>

这个怎么样?

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

很简单。

You may be interested in having a look at the 来源.

我还想建议一个简单的解决方案,当只有一个函数,你知道你会调用(例如: 搜索)

这是我在我的项目中所做的

let throttle;


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

Search is called on input change event

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

Simple throttle function -

注意-继续点击按钮,你会看到控制台日志首先点击,然后只有在每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));

我们还可以使用一个标志来实现-

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

我在这里看到了很多答案,它们对于“一个简单的 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
}
}

这里是一个有点现代化和简化版本的 @clément-prévost answer

function throttle(func, wait, options = {}) {
let timeout = null;
let previous = 0;


const later = (...args) => {
previous = options.leading === false ? 0 : Date.now();
func(...args);
};


return (...args) => {
const now = Date.now();


if (!previous && options.leading === false) {
previous = now;
}


const remaining = wait - (now - previous);


if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
func(...args);
} else if (options.trailing !== false) {
clearTimeout(timeout);
timeout = setTimeout(() => later(...args), remaining);
}
};
}


function myFunc(a) {
console.log(`Log: ${a} ${this.val}`);
}


const myFuncThrottled = throttle(myFunc.bind({val: 42}), 1234, {leading: true, trailing: true})


myFuncThrottled(1)
myFuncThrottled(2)
myFuncThrottled(3)

代码沙盒

const { now } = Date;


export default function throttle(func, frameDuration) {
let timeout = null;
let latest;
const epoch = now();


function getDurationToNextFrame() {
const elapsed = now() - epoch;
const durationSinceLastFrame = elapsed % frameDuration;
return frameDuration - durationSinceLastFrame;
}


function throttled(...args) {
latest = () => {
func.apply(this, args);
};
if (!timeout) {
timeout = setTimeout(() => {
latest();
timeout = null;
}, getDurationToNextFrame());
}
}


return throttled;
}

ES6.代码演示中的简单解决方案

const handleOnClick = () => {
console.log("hello")
}


const throttle = (func, delay) => {
let timeout = null;


return function (...args) {
if (timeout === null) {
func.apply(this, args);
      

timeout = setTimeout(() => {
timeout = null;
}, delay)
}
}
}


document.querySelector("#button").addEventListener("click", throttle(handleOnClick, 500))
<button type="button" id="button">Click me</button>

function throttle(CB,ms=300,Id='Identifier for the callback(CB)'){
Id = Id || ""+CB
var N = throttle.N = throttle.N || {};  // Static variable N to store all callbacks ids and their status
if( N[Id] ) return;             // already in the queue to run
N[Id] = 1;                      // add it the queue
setTimeout(()=>{
N[Id] = 0;                    // remove it from the queue
CB();                         // finally call the function
}, ms);
}






for(var i=0;i<100;i++){
throttle(e=>console.log("Hi1"),1e3,'F1');
}


// will only  output : Hi1
// this function guarantee the callback to run at least once

这里已经有了一些很棒的解决方案,但是我正在寻找一个现代版本,其中包括跟踪(可选地引导)执行,以及为每个函数调用提供的最后传递的参数:

const throttle = (fn, wait=500, leading=true) => {
let prev, timeout, lastargs;
return (...args) => {
lastargs = args;
if (timeout) return;
timeout = setTimeout(() => {
timeout = null;
prev = Date.now();
// let's do this ... we'll release the stored args as we pass them through
fn.apply(this, lastargs.splice(0, lastargs.length));
// some fancy timing logic to allow leading / sub-offset waiting periods
}, leading ? prev && Math.max(0, wait - Date.now() + prev) || 0 : wait);
};
}

用法:

x = throttle((...args) => console.log(...args));
let n = 0;
x(++n, 'boom');
x(++n, 'boom');
x(++n, 'boom');

如果将有一个以上的函数定义他们一个一个将不可维护,所以我会建议使用一个助手类保持每个值

class slowDown {
constructor(cb,timeGap){
this.last = 0
this.run = function(){
let current = Date.now(),
shouldRun = (current - this.last) >= timeGap
if(shouldRun){
cb(current - this.last)
this.last = current
}
}
}
}


// example use
const press = new slowDown(timeElapsed => {
// define function here which you wanted to slow down
console.log("pressed after " + timeElapsed + " ms")
},750)


window.addEventListener("keydown",()=>{
press.run()
})

引导和拖尾的调用:

const throttle = (fn, ms) => {
let locked = false


return function () {
if (!locked) {
locked = true
fn.apply(this, arguments)


setTimeout(() => {
fn.apply(this, arguments)
locked = false
}, ms)
}
}
}

测试案例:

function log({ gender, address }) {
console.log({
name: this.name,
gender,
address,
})
}


const jack = {
name: 'Jack',
log: throttle(log, 3000),
}


Array.from({ length: 5 }, () => jack.log({ gender: 'Male', address: 'LA' }))