Сравнение асинхронной библиотеки Node.js — Q против Async

Я использовал библиотеку Q kriskowal для проекта (веб-скрапер/симулятор человеческой деятельности) и познакомился с промисами, возвращая их и их разрешение/отклонение, а также базовые методы асинхронного потока управления библиотеки и механизмы выдачи/отлова ошибок оказались важными.

Однако я столкнулся с некоторыми проблемами. Мои promise.then вызовы и мои обратные вызовы имеют сверхъестественную тенденцию образовывать пирамиды. Иногда это делается из соображений масштаба, а иногда — чтобы гарантировать определенный порядок событий. (Я полагаю, что смогу исправить некоторые из этих проблем с помощью рефакторинга, но в дальнейшем я хочу полностью избежать «ада обратных вызовов».)

Кроме того, отладка очень расстраивает. Я трачу много времени на то, чтобы console.log найти источник ошибок и багов; после того, как я, наконец, найду их, я начну выбрасывать ошибки туда и ловить их где-то еще с помощью promise.finally, но процесс обнаружения ошибок в первую очередь труден.

Кроме того, в моем проекте порядок имеет значение. Мне нужно сделать почти все последовательно. Часто я ловлю себя на том, что генерирую массивы функций, которые возвращают промисы, а затем связываю их друг с другом, используя Array.prototype.reduce, что, как мне кажется, мне не нужно делать.

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

removeItem: function (itemId) {

  var removeRegexp = new RegExp('\\/stock\\.php\\?remove=' + itemId);

  return this.getPage('/stock.php')
  .then(function (webpage) {
    var
      pageCount = 5,
      promiseFunctions = [],
      promiseSequence;

    // Create an array of promise-yielding functions that can run sequentially.
    _.times(pageCount, function (i) {
      var promiseFunction = function () {
        var
          promise,
          path;

        if (i === 0) {
          promise = Q(webpage);
        } else {
          path = '/stock.php?p=' + i;
          promise = this.getPage(path);
        }

        return promise.then(function (webpage) {
          var
            removeMatch = webpage.match(removeRegexp),
            removePath;

          if (removeMatch !== null) {
            removePath = removeitemMatch[0];

            return this.getPage(removePath)
            .delay(1000)
            // Stop calling subsequent promises.
            .thenResolve(true);
          }

          // Don't stop calling subsequent promises.
          return false;

        }.bind(this));
      }.bind(this);

      promiseFunctions.push(promiseFunction);
    }, this);

    // Resolve the promises sequentially but stop early if the item is found.
    promiseSequence = promiseFunctions.reduce(function (soFar, promiseFunction, index) {
      return soFar.then(function (stop) {
        if (stop) {
          return true;
        } else {
          return Q.delay(1000).then(promiseFunction);
        }
      });
    }, Q());

    return promiseSequence;
  }.bind(this))
  .fail(function (onRejected) {
    console.log(onRejected);
  });
},

У меня есть другие методы, которые делают в основном то же самое, но страдают от гораздо более серьезных проблем с отступами.

Я рассматриваю возможность рефакторинга моего проекта с использованием асинхронной библиотеки coalan. Это похоже на Q, но я хочу точно знать, чем они отличаются. У меня сложилось впечатление, что async более «ориентирован на обратный вызов», а Q - «ориентирован на обещания».

Вопрос. Учитывая мои проблемы и требования к проекту, что я выиграю и/или потеряю, используя асинхронность вместо Q? Как библиотеки сравниваются? (Особенно с точки зрения последовательного выполнения ряда задач и отладки/обработки ошибок?)


person Jackson    schedule 19.03.2014    source источник
comment
Требование последовательного выполнения сводит на нет большинство преимуществ асинхронности.   -  person Robert Harvey    schedule 19.03.2014
comment
Люди, вероятно, могли бы посоветовать вам лучше, если бы вы показали особенно громоздкий фрагмент кода, который вы используете сейчас, для которого вам нужно лучшее решение. Обсуждать плюсы и минусы разных библиотек или то, как эти библиотеки используются, гораздо сложнее в абстрактном плане.   -  person jfriend00    schedule 19.03.2014
comment
@jfriend00 Я согласен; Я добавил пример кода.   -  person Jackson    schedule 19.03.2014
comment
пирамиды могут быть уменьшены/удалены за счет правильного использования .then. асинхронная библиотека, с другой стороны, имеет несколько методов, которые можно использовать для синхронного выполнения асинхронных вызовов, таких как .series и .eachSeries. цепь .then может, конечно, достичь той же цели   -  person Kevin B    schedule 04.08.2014
comment
@Robert Harvey - в узле асинхронность по-прежнему ценна, потому что она дает возможность обрабатывать другие запросы.   -  person Robert Levy    schedule 04.08.2014
comment
Вы проверили async-q? Это обертка промисов (использующая Q) вокруг асинхронной библиотеки Coalan. По сути, он добавляет больше структур управления для асинхронного кода, такого как карта, сокращение, серия и т. д.: npmjs.com/package /асинхронный-q   -  person slebetman    schedule 30.09.2016
comment
@slebetman Круто! Я хочу попробовать это сейчас.   -  person Jackson    schedule 30.09.2016


Ответы (3)


Обе библиотеки хороши. Я обнаружил, что они служат разным целям и могут использоваться в тандеме.

Q предоставляет разработчику объекты обещаний, которые являются будущими представлениями значений. Полезно для путешествий во времени.

Async предоставляет разработчику асинхронные версии управляющих структур и агрегатных операций.

Пример из одной попытки реализации линтера демонстрирует потенциальное единство библиотек:

function lint(files, callback) {

    // Function which returns a promise.
    var getMerged = merger('.jslintrc'),

        // Result objects to invoke callback with.
        results = [];

    async.each(files, function (file, callback) {
        fs.exists(file, function (exists) {

            // Future representation of the file's contents.
            var contentsPromise,

                // Future representation of JSLINT options from .jslintrc files.
                optionPromise;

            if (!exists) {
                callback();
                return;
            }

            contentsPromise = q.nfcall(fs.readFile, file, 'utf8');
            optionPromise = getMerged(path.dirname(file));

            // Parallelize IO operations.
            q.all([contentsPromise, optionPromise])
                .spread(function (contents, option) {
                    var success = JSLINT(contents, option),
                        errors,
                        fileResults;
                    if (!success) {
                        errors = JSLINT.data().errors;
                        fileResults = errors.reduce(function (soFar, error) {
                            if (error === null) {
                                return soFar;
                            }
                            return soFar.concat({
                                file: file,
                                error: error
                            });
                        }, []);
                        results = results.concat(fileResults);
                    }
                    process.nextTick(callback);
                })
                .catch(function (error) {
                    process.nextTick(function () {
                        callback(error);
                    });
                })
                .done();
        });
    }, function (error) {
        results = results.sort(function (a, b) {
            return a.file.charCodeAt(0) - b.file.charCodeAt(0);
        });
        callback(error, results);
    });
}

Я хочу сделать что-то потенциально блокирующее для каждого файла. Так что async.each - очевидный выбор. Я могу распараллелить связанные операции на итерацию с помощью q.all и повторно использовать значения параметров, если они применяются к 2 или более файлам.

Здесь Async и Q влияют на поток управления программой, а Q представляет значения, разрешающие содержимое файла когда-нибудь в будущем. Библиотеки хорошо работают вместе. Не нужно «выбирать одно над другим».

person Jackson    schedule 12.11.2014

Пирамиды обратного вызова в вашем коде можно упростить, используя композицию промисов и лексическую область видимости javascript.

removeItem: function (itemId) {

  var removeRegexp = new RegExp('\\/stock\\.php\\?remove=' + itemId);
  var found = false
  var promise = getPage('/sock.php')

  _.times(5, (i) => {
    promise = promise.then((webpage) => {
      if (found) return true
      var removeMatch = webpage.match(removeRegexp)
      var found = removeMath !== null
      var nextPage = found ? removeMatch[0] : '/stock.php?p='+i+1
      return Q.delay(1000).then(() => this.getPage(nextPage))
    })
  })

  return promise.fail(console.log.bind(console))

},

ИМХО async не следует использовать в новом коде javascript. Обещания более компонуемые и позволяют использовать гораздо более интуитивно понятный код.

Основная причина, по которой Node.js не использовал промисы, заключалась в проблемах с производительностью, которые в значительной степени были очень хорошо решены такими библиотеками, как Bluebird и Q.

По мере того, как синтаксис async/await становится все более популярным, промисы проложат путь для кода, очень похожего на синхронный код.

person lorefnon    schedule 19.12.2015

Хотя это еще не ответ на мой вопрос (Q vs async), относительно моей проблемы я нашел Selenium/WebDriverJs как жизнеспособное решение.

driver.get('http://www.google.com');
driver.findElement(webdriver.By.name('q')).sendKeys('webdriver');
driver.findElement(webdriver.By.name('btnG')).click();
driver.wait(function() {
  return driver.getTitle().then(function(title) {
    return title === 'webdriver - Google Search';
  });
}, 1000);

WebDriver использует очередь для последовательного выполнения промисов, что очень помогает контролировать отступы. Его обещания также совместимы с Q.

Создание последовательности обещаний больше не проблема. Подойдет простой цикл for.

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

person Jackson    schedule 03.08.2014
comment
Должен сказать, я никогда не думал об использовании селена для парсинга, я использую его для тестирования в качестве симулятора человеческой деятельности, что интересно. Также обещания веб-драйвера несовместимы с Q, они оцениваются по-разному и могут дать вам ложные срабатывания. Если вам нужно создать собственные промисы, используйте webdriver.promise(). - person RadleyMith; 21.01.2015