Управление очередью в JavaScript через обратные вызовы

Я работаю над страницей, использующей JavaScript для управления очередью. Моя проблема заключается в том, что мой код имеет вложенные обратные вызовы. Вложенные обратные вызовы меня путают в отношении объема моей очереди. В настоящее время у меня есть следующее:

function MyApp() {}
module.exports = MyApp;

MyApp.myQueue = [];
MyApp.queueIsLocked = false;

MyApp.enqueue = function(item, onSuccess, onFailure) {
  if (!MyApp.queueIsLocked) {
    MyApp.queueIsLocked = true;        
    MyApp.myQueue.push(item);
    MyApp.queueIsLocked = false;

    item.send(   
      function() {
        console.log('item: ' + item.id);

        MyApp.queueIsLocked = true;                      
        MyApp.findItemById(item.id,
          function(index) {
            if (index !== -1) {
              MyApp.myQueue.splice(index, 1);
              MyApp.queueIsLocked = false;

              if (onSuccess) {
                onSuccess(item.id);
              }
            }
          }
        );
      },
      function() {
        alert('Unable to send item to the server.');
        if (onFailure) {
          onFailure();
        }
      }
    );
  }
};

MyApp.findItemById = function(id, onComplete) {
  var index = -1;
  if (MyApp.queueIsLocked) {
    setTimeout(function() {
      // Attempt to find the index again.
    }, 100);
  } else {
    MyApp.queueIsLocked = true;
    for (var i=0; i<MyApp.myQueue.length; i++) {
      if (MyApp.myQueue[i].id === id) {
        index = i;
        break;
      }
    }
  }

  if (onComplete) {
    onComplete(index);
  }
};

Функция send ведет себя по-разному в зависимости от деталей item. Иногда элемент отправляется на один сервер. Иногда он будет отправлен на несколько серверов. В любом случае, я не знаю, когда элемент будет выполнен "отправлен". По этой причине я использую обратный вызов для управления очередью. Когда элемент выполняется "отправлено", я хочу удалить его из очереди. Мне нужно использовать тайм-аут или интервал, чтобы проверить, заблокирована ли очередь или нет. Если он не заблокирован, я хочу удалить элемент из очереди. Эта проверка добавляет еще один уровень вложенности, который меня путает.

Моя проблема в том, что я не верю, что объем индекса работает так, как я ожидал. Я чувствую, что у меня состояние гонки. Я основываю это на том факте, что я написал следующий тест Жасмина:

describe('Queue', function() {
  describe('Approach 1', function() {
    it('should do something', function() {
      MyApp.enqueue({id:'QRA', text:'Test A'});
    });
  });

  describe('Approach 2', function() {
    it('should successfully queue and dequeue items', function() {
      MyApp.enqueue({id:'WX1', text:'Test 1'});
      MyApp.enqueue({id:'QV2', text:'Test 2'});
      MyApp.enqueue({id:'ZE3', text:'Test 3'});
    });
  });
});

При выполнении теста я вижу следующее в окне консоли:

item: QRA index: 1
item: WX1 index: 2
item: QV2 index: 3
item: ZE3 index: 4

Похоже, что элементы не будут удалены, как я ожидал. Неужели я нахожусь в основе моего подхода к управлению очередью? Что я делаю неправильно?

Спасибо за любую помощь.

Ответ 1

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

  • Похоже, что очередь представляет элементы, которые вы пытаетесь отправить на сервер. Вы добавляете элементы в очередь, которую необходимо отправить, и удаляете их из очереди после того, как они были успешно отправлены.
  • Вы хотите, чтобы ваш код одновременно отправлял несколько элементов одновременно? Например, элемент A добавляется в очередь, а затем отправляется. Перед асинхронной отправкой для финишей A в список добавляется элемент B. Должен ли код попытаться отправить элемент B перед отправкой товара A? На основе вашего кода это звучит как "да".

Кажется, что вам действительно не нужна/нужна очередь, как таковая, так как вы хотите, чтобы список отслеживал, какие элементы находятся в процессе отправки. "Очередь" подразумевает, что объекты обрабатываются в некотором порядке FIFO.

Если вы просто хотите отслеживать элементы на основе идентификатора, тогда вы можете использовать объект. Например:

MyApp.items = {};
MyApp.addItem = function(item){
  MyApp.items[item.id] = item;
  item.send(
    function(){ // success
      MyApp.removeItem(item.id)
    }
  );
}
MyApp.removeItem = function(id){
  delete MyApp.items[id];
  onSuccess(id);
}

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

Ответ 2

Большой недостаток, который я вижу, заключается в том, что вы вызываете MyApp.queueIsLocked = true непосредственно перед MyApp.findItemById. Поскольку он заблокирован, функция устанавливает тайм-аут (ничего не делает) и переходит к вызову onComplete(-1). -1 затем явно игнорируется onComplete, не удаляя dequeue и блокируя вашу очередь.

Вероятно, вы хотели повторить поиск, например:

setTimeout(function() {
  // Attempt to find the index again.
  MyApp.findItemById(id, onComplete);
}, 100);

Я не уверен, но я думаю, что Жасмин требует явной инструкции для запуска функций Timeout, используя jasmine.clock().tick


Тем не менее, я предлагаю удалить все ссылки на queueIsLocked, включая указанный выше код таймаута. Кроме того, если item.id всегда является уникальной строкой, вы можете использовать объект вместо массива для хранения ваших значений.

Вот предлагаемая итерация, как можно более верная исходному API:

function MyApp() {}
module.exports = MyApp;

MyApp.myQueue = {};

//Sends the item, calling onSuccess or onFailure when finished
//  item will appear in MyApp.myQueue while waiting for a response from send
MyApp.enqueue = function(item, onSuccess, onFailure) {
  MyApp.myQueue[item.id] = item;

  item.send(function() {
    console.log('item: ' + item.id);
    delete MyApp.myQueue[item.id];
    if (onSuccess) {
      onSuccess(item.id);
    }
  }, function() {
    alert('Unable to send item to the server.');
    if (onFailure) {
      onFailure();
    }
  });
};

//Returns the Item in the queue, or undefined if not found
MyApp.findItemById = function(id, onComplete) {
  if (onComplete) {
    onComplete(id);
  }
  return MyApp.myQueue[id];
};

Ответ 3

Попробуйте использовать ECMA 6 Promise или любое обещание от js framework.Promiseis более подходит для этой задачи. подробнее см. https://developer.mozilla.org/

function MyApp() {}
module.exports = MyApp;

MyApp.myQueue = [];
MyApp.queueIsLocked = false;

MyApp.enqueue = function(item) {
 return new Promise(function(resolve, reject) {
      if (!MyApp.queueIsLocked) {
        MyApp.queueIsLocked = true;        
        MyApp.myQueue.push(item);
        MyApp.queueIsLocked = false;

        var onResolve = function() {
            console.log('item: ' + item.id);
            MyApp.queueIsLocked = true;   
            MyApp.findItemById(item.id).then(function(index){
                 if (index !== -1) {
                      MyApp.myQueue.splice(index, 1);
                      MyApp.queueIsLocked = false;
                      resolve(item.id);
                 }
            });     

        };

        item.send(onResolve,reject);
      }
  });
};

MyApp.findItemById = function(id) {
     return new Promise(function(resolve, reject) {
              var index = -1;
              if (MyApp.queueIsLocked) {
                setTimeout(function() {
                  // Attempt to find the index again.
                }, 100);
              } else {
                MyApp.queueIsLocked = true;
                for (var i=0; i<MyApp.myQueue.length; i++) {
                  if (MyApp.myQueue[i].id === id) {
                    index = i;
                    break;
                  }
                }
                resolve(index);
              }
        });
};

Ответ 4

переместите MyApp.queueIsLocked = false; на обратный вызов отправки сервера