Семафорподобная очередь в javascript?

У меня есть переменная can_run, которая может быть 1 или 0, а затем у меня есть очередь функций, которая должна запускаться, как только переменная переключается с 0 на 1 (но только 1 такая функция за раз).

Сейчас я делаю

var can_run=1;
function wait_until_can_run(callback) {
    if (can_run==1) {
        callback();
    } else {
        window.setTimeout(function(){wait_until_can_run(callback)},100);
    }
}

//...somewhere else...

wait_until_can_run( function(){
   can_run=0;
   //start running something
});

//..somewhere else, as a reaction to the task finishing..
can_run=1;

Это работает, однако, это не влияет на меня, так как очень эффективно, чтобы иметь около 100 тайм-аутов, постоянно работающих. Что-то вроде семафора было бы удобно здесь; но в общем случае семафоры на самом деле не нужны в JavaScript.

Итак, что использовать здесь?

edit: Я написал "очередь функций", но, как видно здесь, меня не очень беспокоит порядок.

Ответ 1

Вот хороший класс Queue, который вы можете использовать без времени ожидания:

var Queue = (function () {

    Queue.prototype.autorun = true;
    Queue.prototype.running = false;
    Queue.prototype.queue = [];

    function Queue(autorun) {
        if (typeof autorun !== "undefined") {
            this.autorun = autorun;
        }
        this.queue = []; //initialize the queue
    };

    Queue.prototype.add = function (callback) {
        var _this = this;
        //add callback to the queue
        this.queue.push(function () {
            var finished = callback();
            if (typeof finished === "undefined" || finished) {
                //  if callback returns 'false', then you have to 
                //  call 'next' somewhere in the callback
                _this.dequeue();
            }
        });

        if (this.autorun && !this.running) {
            // if nothing is running, then start the engines!
            this.dequeue();
        }

        return this; // for chaining fun!
    };

    Queue.prototype.dequeue = function () {
        this.running = false;
        //get the first element off the queue
        var shift = this.queue.shift();
        if (shift) {
            this.running = true;
            shift();
        }
        return shift;
    };

    Queue.prototype.next = Queue.prototype.dequeue;

    return Queue;

})();

Его можно использовать так:

// passing false into the constructor makes it so 
// the queue does not start till we tell it to
var q = new Queue(false).add(function () {
    //start running something
}).add(function () {
    //start running something 2
}).add(function () {
    //start running something 3
});

setTimeout(function () {
    // start the queue
    q.next();
}, 2000);

Демонстрация скрипки: http://jsfiddle.net/maniator/dUVGX/


Обновлено для использования es6 и новых обещаний es6:

class Queue {  
  constructor(autorun = true, queue = []) {
    this.running = false;
    this.autorun = autorun;
    this.queue = queue;
  }

  add(cb) {
    this.queue.push((value) => {
        const finished = new Promise((resolve, reject) => {
        const callbackResponse = cb(value);

        if (callbackResponse !== false) {
            resolve(callbackResponse);
        } else {
            reject(callbackResponse);
        }
      });

      finished.then(this.dequeue.bind(this), (() => {}));
    });

    if (this.autorun && !this.running) {
        this.dequeue();
    }

    return this;
  }

  dequeue(value) {
    this.running = this.queue.shift();

    if (this.running) {
        this.running(value);
    }

    return this.running;
  }

  get next() {
    return this.dequeue;
  }
}

Его можно использовать таким же образом:

const q = new Queue(false).add(() => {
    console.log('this is a test');

    return {'banana': 42};
}).add((obj) => {
    console.log('test 2', obj);

    return obj.banana;
}).add((number) => {
    console.log('THIS IS A NUMBER', number)
});

// start the sequence
setTimeout(() => q.next(), 2000);

Хотя на этот раз, если переданные значения являются обещанием и т.д. или значением, оно автоматически передается следующей функции.

Скрипки: http://jsfiddle.net/maniator/toefqpsc/

Ответ 2

Я не уверен, что лучший способ сделать это в простой JS, но во многих библиотеках есть варианты Deferred, которые очень полезны для этого случая использования.

С помощью jQuery:

var dfd = $.Deferred();
var callback = function() { 
    // do stuff
};
dfd.done(callback);  // when the deferred is resolved, invoke the callback, you can chain many callbacks here if needed
dfd.resolve(); // this will invoke your callback when you're ready

РЕДАКТИРОВАТЬ. Одна из приятных вещей об этих отложенных библиотеках - это то, что они обычно совместимы с событиями Ajax и, в свою очередь, с другими объектами с отсрочкой, поэтому вы можете создавать сложные цепи, запускать события в Ajax complete, или инициировать обратный вызов "done" после выполнения нескольких условий. Это, конечно, более продвинутые функции, но приятно иметь в заднем кармане.

Ответ 3

В дополнение к другим полезным ответам здесь, если вы не нужны дополнительные услуги, предоставляемые этими решениями, затем асинхронный семафор можно реализовать.

Это концепция более низкого уровня, чем другие варианты здесь, тем не менее, вы можете найти те, которые будут более удобны для твои нужды. Тем не менее, я считаю, что асинхронные семафоры стоят зная об этом, даже если вы используете абстракции более высокого уровня в практика.

Это выглядит примерно так:

var sem = function(f){
    var busy = 0;
    return function(amount){
        busy += amount;
        if(busy === 0){
            f();
        }
    };
};

И вы вызываете его так:

var busy = sem(run_me_asap);

busy - это функция, которая поддерживает внутренний счетчик асинхронные действия, которые он ожидает. Когда это внутренний счетчик достигает нуля, он запускает функцию run_me_asap, который вы поставляете.

Вы можете увеличить внутренний счетчик до запуска асинхронное действие с busy(1), а затем асинхронные действия отвечают за уменьшение счетчик с busy(-1) после его завершения. Вот как мы может избежать необходимости в таймерах. (Если вы предпочитаете, вы можете напишите sem, чтобы он возвращал объект с inc и dec вместо этого, как в статье в Википедии; это как я это делаю.)

И все, что вам нужно сделать, чтобы создать асинхронный семафор.

Вот пример его использования. Вы можете определить функцию run_me_asap следующим образом.

var funcs = [func1, func2, func3 /*, ...*/];
var run_me_asap = function(){
    funcs.forEach(function(func){
        func();
    });
});

funcs может быть список функций, которые вы хотели запустите в своем вопросе. (Может быть, это не совсем то, что вы хотите, но см. мой "N.B." ниже.)

Тогда в другом месте:

var wait_until_ive_finished = function(){
    busy(1);
    do_something_asynchronously_then_run_callback(function(){
        /* ... */
        busy(-1);
    });
    busy(1);
    do_something_else_asynchronously(function(){
        /* ... */
        busy(-1);
    });
};

Когда оба асинхронных операции завершены, busy счетчик будет установлен на ноль, а run_me_asap будет вызывается.

N.B.. Как вы можете использовать асинхронные семафоры по архитектуре вашего кода и вашим собственным требованиям; то, что я изложил, может быть не совсем то, что вы хотите. Я просто пытаясь показать вам, как они работают; остальное зависит от вас!

И, одно слово совета: если бы вы использовали асинхронный семафоров, то я бы рекомендовал вам скрыть свое творение и вызовы busy за абстракциями более высокого уровня, поэтому что вы не засоряете свой код приложения детали низкого уровня.