Node.js异步库比较 - Q vs Async

28
我曾经在一个项目(网络爬虫/人类活动模拟器)中使用过 kriskowal的Q库,并且熟悉了Promise、如何返回Promise以及如何解决/拒绝Promise。该库的基本异步控制流方法和错误抛出/捕获机制已被证明是必不可少的。
然而,我遇到了一些问题。我的promise.then调用和回调函数有时倾向于形成金字塔结构。有时是由于作用域的原因,有时是为了保证一定的事件顺序。(我想通过重构来解决这些问题,但今后我希望完全避免“回调地狱”)
此外,调试非常令人沮丧。我花了很多时间用console.log寻找错误和漏洞的源头;当我最终找到它们时,我会在那里抛出错误,并在其他地方使用promise.finally捕获它们,但首先定位错误的过程是艰难的。
此外,在我的项目中,顺序很重要。我几乎需要按顺序执行所有操作。往往我会生成返回Promise的函数数组,然后使用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的async库 重构我的项目。它看起来与Q类似,但我想知道它们之间的确切区别。我得到的印象是,async更加“回调中心”,而Q则是“承诺中心”。

问题:鉴于我的问题和项目要求,如果使用async而不是Q,我会获得什么,失去什么?这两个库有什么区别?(特别是在按顺序执行一系列任务和调试/错误处理方面?)


3
要求顺序执行似乎会使大部分异步的好处失效。 - Robert Harvey
如果您展示一段特别难以操作的代码,并表达出您希望获得更好的解决方案,那么其他人可能会更好地向您提供建议。在抽象的情况下讨论不同库的优缺点或如何使用这些库要困难得多。 - jfriend00
@jfriend00 我同意;我已经添加了一个代码示例。 - Jackson
2
“金字塔”可以通过正确使用.then来减少/删除。另一方面,async库有几种方法可用于将异步调用同步执行,例如.series.eachSeries。当然,.then链也可以实现相同的目标。 - Kevin B
@Robert Harvey - 在Node中,异步仍然很有价值,因为它可以让其他请求有机会被处理。 - Robert Levy
显示剩余2条评论
3个回答

18

两个库都很好。我发现它们有不同的用途,可以一起使用。

Q为开发人员提供了Promise对象,这些对象是值的未来表示形式。对于时间旅行非常有用。

Async为开发人员提供了控制结构和聚合操作的异步版本。

一个尝试实现linter的示例展示了库之间潜在的统一性:

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 并行地处理每个迭代中的相关操作,并且如果选项值适用于两个或多个文件,则可以重复使用。

在此代码中,Async 和 Q 均影响程序的控制流程,Q 表示将来某个时候解析为文件内容的值。这些库很好地配合使用,你不需要“选择其中之一”。


2

使用Promise组合和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))

},

我认为在新的javascript代码中不应该使用async。Promises更具可组合性,可以编写更直观的代码。

node最初没有使用promises的主要原因是出于性能方面的考虑,但是像Bluebird和Q这样的库已经很好地解决了这个问题。

随着async/await语法变得越来越流行,promises将为类似于同步代码的代码铺平道路。


-1
虽然这仍不是我的问题(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循环就可以了。
至于在序列中提前停止,请不要这样做。不要使用序列,而要使用异步while设计和分支。

1
我必须说,我从未想过使用Selenium进行爬取,我将其用于测试作为人类活动模拟器,不过这很有趣。此外,webdriver的承诺与Q的不兼容,它们评估方式不同,可能会给您带来错误的结果。如果您需要创建自定义承诺,请使用webdriver.promise()。 - RadleyMith

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