限制JavaScript函数速率

9
我该如何限制一个函数每秒只运行10次,但当有新的“位置”可用时就继续执行呢?这意味着我们会尽快调用函数10次,并且当距离任何函数调用已经过去1秒时,我们可以再做一次调用。 这个描述可能有点混乱 - 但是答案将是在给定速率限制下完成X个API调用的最快方式。
例如: 这里有一个例子,循环遍历字母表以打印每个字母。 我们怎样才能将其限制为每秒只调用10次printLetter? 我仍然希望以适当的速率循环遍历所有字母。
function printLetter(letter){
  console.log(letter);
}

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

// How can I limit this to only run 10 times per second, still loop through every letter, and complete as fast as possible (i.e. not add a hard spacing of 100ms)?
alphabet.forEach(function(letter){
  printLetter(letter);
});

一个好的解决方案不会强制将每个调用间隔100ms。这使得10个调用的最短运行时间为1秒,而实际上你可以几乎同时完成这些调用,并在一小部分时间内完成。

4
10秒是一个硬限制吗?否则,你可以利用setInterval函数。 - Lucas Pottersky
1
@AndersonGreen 那个重复的目标没有任何真正可用的解决方案。最受欢迎的答案需要使用第三方库。 - user4639281
3
“but continue execution when new 'spots' are available” 的意思是“但在新的“空位”可用时继续执行”。 - Andy
1
@RacilHilan 并不是所有的答案都使用这种方法,而且这也不是限制函数调用速率的唯一方法,正如我在下面的回答中所示。 - user4639281
1
感谢您进行的第二次编辑,它真正地澄清了事情。 - nathanhleung
显示剩余7条评论
7个回答

11
大多数其他提出的解决方案都使用间隔或递归函数与超时时间来均匀分配函数调用。
这个问题的解释并没有真正做到我认为你所要求的,因为它需要你按照一定的时间间隔调用函数。
如果您想限制函数被调用的次数而不考虑函数调用之间的时间间隔,可以使用以下方法。
定义一个工厂函数来保存当前时间、计数和队列,然后返回一个函数,该函数检查当前时间与上次记录的当前时间和计数,然后执行队列中的第一个项,或等待下一秒再尝试。
将回调函数传递给由工厂函数创建的函数。回调函数将被输入到一个队列中。限制函数执行队列中的前10个函数,然后等待此间隔完成后再执行下一个10个函数,直到队列为空。
从工厂函数返回限制函数。

var factory = function(){
    var time = 0, count = 0, difference = 0, queue = [];
    return function limit(func){
        if(func) queue.push(func);
        difference = 1000 - (window.performance.now() - time);
        if(difference <= 0) {
            time = window.performance.now();
            count = 0;
        }
        if(++count <= 10) (queue.shift())();
        else setTimeout(limit, difference);
    };
};

var limited = factory();
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");

// This is to show a separator when waiting.
var prevDate = window.performance.now(), difference;

// This ends up as 2600 function calls, 
// all executed in the order in which they were queued.
for(var i = 0; i < 100; ++i) {
    alphabet.forEach(function(letter) {
        limited(function(){
            /** This is to show a separator when waiting. **/
            difference = window.performance.now() - prevDate;
            prevDate   = window.performance.now();
            if(difference > 100) console.log('wait');
            /***********************************************/
            console.log(letter);
        });
    });
}


3
你需要稍微做一些不同的事情:
var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

function printLetter(letterId) {
    if (letterId < alphabet.length) { // avoid index out of bounds

        console.log(alphabet[letterId]);

        var nextId = letterId + 1
        if (nextId < alphabet.length) // if there is a next letter print it in 10 seconds
            setTimeout("printLetter(" + nextId + ")", 10000/*milliseconds*/);
    }
}

printLetter(0); // start at the first letter

Demo:

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

function printLetter(letterId) {
  if (letterId < alphabet.length) { // avoid index out of bounds

    console.log(alphabet[letterId]);
    document.body.innerHTML += "<br />" + alphabet[letterId]; // for ***DEMO*** only

    var nextId = letterId + 1
    if (nextId < alphabet.length) // if there is a next letter print it in 10 seconds
      setTimeout("printLetter(" + nextId + ")", 100 /*milliseconds*/ ); // one second for ***DEMO*** only
  }
}

printLetter(0); // start at the first letter


不知道是谁在负面评价。无论如何,我点了赞以进行抵消,而且我还很感激你抽出时间来回答问题。现在我正在看你的回答 :) - Don P
如果你想要迭代10次,那么你应该将1000改为100。附注:ps。 - Andy
1
谢谢!如果您有更多问题,请回来;) - CoderPi

1

递归版本总是看起来更酷

// Print the first letter, wait, and do it again on a sub array until array == []
// All wrapped up in a self-invoking function

var alphabet = ...
var ms      = 100 // 10 letters per seconds

(function printSlowly( array, speed ){

    if( array.length == 0 ) return; 

    setTimeout(function(){
        console.log( array[0] );
        printSlowly( array.slice(1), speed );
    }, speed );

})( alphabet, ms);

除非你的代码是递归的,否则它会将执行返回给setTimeout调用者,即定时器进程,而不是printSlowly(虽然这不是我的投票)。 - Teemu
1
它是间接递归的,因此也是递归的。 - Ben Aston
你也定义了 ms 但是在函数中却传入了 500 - Andy
2
这没有最大限制使用速率。应该立即启动10次运行(因为用户仍然应尽快收到响应)。完成 10 次运行的最快时间至少为 1 秒。 - Don P

0
您可以使用值为100的setTimeout(即1000毫秒/10)来限制每秒输出10次。使用一个变量call来计数调用次数。如果您想在其他地方调用相同的函数,请记得将计数器call重置为1,以便重新开始:

var call = 1;

function printLetter(letter){
  call++;
  var x = call * 100;
  //alert(x);
  setTimeout(function(){ 
    document.getElementById("test").innerHTML += letter;
  }, x);
}

var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

// How can I limit this to only run 10 times per second, but still loop through every letter?
alphabet.forEach(function(letter){
  printLetter(letter);
});
<div id="test"/>


如果你使用setTimeout,就没有必要使用forEach - Andy
@Andy 有无数种方法可以做某件事情。如果你让我按照我的方式编码,那么它会完全不同,但我现在不是按照我的方式来做,而是按照 OP 的方式来做,因为这是他的代码和项目,而不是我的。他问了一个具体的问题:如何将函数限制在每秒钟执行10次,所以我修改了函数以满足他的要求。我没有触及任何其他部分,因为我们都不知道其他部分应该做什么以及他将如何使用它。 - Racil Hilan
2
好的,但这并不高效。另外,为什么你要在每次迭代中增加超时值?那没有任何意义:var x = call * 100 - Andy
@Andy 真的吗?如果你提供建议,那么你的 JS 技能一定很好,这意味着你应该能够轻松理解我的简单答案。为什么不把我的答案复制到 Fiddle 中,删除那一行代码,然后看看会发生什么?OP 在循环中调用函数,执行时间几乎是瞬间完成的,所以你几乎在相同的时间内获得了 26 个调用。没有那行代码,setTimeout 将被调用 26 次并具有相同的延迟时间 100 毫秒,因此所有字母在循环后 100 毫秒几乎同时打印出来。现在清楚了吧,我的朋友? - Racil Hilan

0

这是我在有限的时间内能想到的最好方案。

请注意,由于Firefox v43实现箭头函数存在一个错误,因此此代码将无法在其下正确运行。

var MAX_RUNS_PER_WINDOW = 10;
var RUN_WINDOW = 1000;

function limit(fn) {
    var callQueue = [], 
      invokeTimes = Object.create(circularQueue), 
      waitId = null;
    
    function limited() {        
        callQueue.push(() => {
            invokeTimes.unshift(performance.now())
            fn.apply(this, arguments);
        });
                
        if (mayProceed()) {
            return dequeue();
        }
        
        if (waitId === null) {
            waitId = setTimeout(dequeue, timeToWait());
        }
    }

    limited.cancel = function() {
      clearTimeout(waitId);
    };

    return limited;
    
    function dequeue() {
        waitId = null ;
        clearTimeout(waitId);
        callQueue.shift()();
        
        if (mayProceed()) {
            return dequeue();
        }
        
        if (callQueue.length) {
            waitId = setTimeout(dequeue, timeToWait());
        }
    }
    
    function mayProceed() {
        return callQueue.length && (timeForMaxRuns() >= RUN_WINDOW);
    }
    
    function timeToWait() {        
        var ttw = RUN_WINDOW - timeForMaxRuns();
        return ttw < 0 ? 0 : ttw;
    }

    function timeForMaxRuns() {
        return (performance.now() - (invokeTimes[MAX_RUNS_PER_WINDOW - 1] || 0));
    }
}

var circularQueue = [];
var originalUnshift = circularQueue.unshift;

circularQueue.MAX_LENGTH = MAX_RUNS_PER_WINDOW;

circularQueue.unshift = function(element) {
    if (this.length === this.MAX_LENGTH) {
        this.pop();
    }
    return originalUnshift.call(this, element);
}

var printLetter = limit(function(letter) {
    document.write(letter);
});

['A', 'B', 'C', 'D', 'E', 'F', 'G', 
'H', 'I', 'J', 'K', 'L', 'M', 'N', 
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 
'V', 'X', 'Y', 'Z'].forEach(printLetter);


0
这是一个带有回调函数的递归版本(你所说的“在新空位可用时继续执行”是这个意思吗?)
编辑:现在更加抽象 - 如果你想看原始实现(非常具体),请参见http://jsfiddle.net/52wq9vLf/0/
var alphabet = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "X", "Y", "Z"];

/**
 * @function printLetter
 * @param {Array} array The array to iterate over
 * @param {Function} iterateFunc The function called on each item
 * @param {Number} start The index of the array to start on
 * @param {Number} speed The time (in milliseconds) between each iteration
 * @param {Function} done The callback function to be run on finishing
 */

function slowArrayIterate(array, iterateFunc, start, speed, done) {
    // Native array functions use these three params, so just maintaining the standard
    iterateFunc(array[start], start, array);
    if (typeof array[start + 1] !== 'undefined') {
        setTimeout(function() {
            slowArrayIterate(array, iterateFunc, start + 1, speed, done);
        }, speed);
    } else {
        done();
    }
};

slowArrayIterate(alphabet, function(arrayItem) {
    document.getElementById("letters").innerHTML += arrayItem;
}, 0, 100, function() {
    // stuff to do when finished
    document.getElementById("letters").innerHTML += " - done!";
});

这里有一个 JSFiddle:http://jsfiddle.net/52wq9vLf/2/


0

这些都没什么用。我不是一个很好的开发者,只是在做测试时编写了自己的一组小函数。我无法理解所接受的答案在做什么,也许这会帮助其他人。

应该很容易阅读。

var queue = [];
var queueInterval;
var queueCallsPerSecond = 5;

function addToQueue(callback, args) {
    //push this callback to the end of the line.
    queue.push({
        callback: callback,
        args: args
    });

    //if queueInterval isn't running, set it up to run
    if(!queueInterval){

        //first one happens right away
        var nextQueue = queue.shift(); 
        nextQueue.callback(...nextQueue.args);

        queueInterval = setInterval(function(){
            //if queue is empty clear the interval
            if(queue.length === 0) {
                clearInterval(queueInterval);
                return false;
            }

            //take one off, run it
            nextQueue = queue.shift(); 
            nextQueue.callback(...nextQueue.args);

        }, 1000 / queueCallsPerSecond);
    }
}


//implementation addToQueue(callback, arguments to send to the callback when it's time to go) - in this case I'm passing 'i' to an anonymous function.
for(var i = 0; i < 20; i++){
    addToQueue(
        function(num) {
            console.log(num);
        },
        [i]
    );
}

想象一下,你的桌子上有一个托盘,人们把任务放在里面...就像一个收件箱。同事们添加的任务比你执行得快,所以你需要想出一个计划。你总是从堆栈底部开始取,当收件箱为空时,你可以停止寻找下一个任务。这就是它的全部功能。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接