如何在node.js中使用Promise来简化回调链清理?

3
我想弄清楚如何使用Promise,特别是Q实现来清理一些杂乱的嵌套回调在一个Node.js程序中。不幸的是,似乎很少有简单的例子可以说明我想做的事情。
这里是我现在拥有的嵌套回调的简化版本:
    var parent = this;
    this.receiveMessage(params, function(err, request) {
    if (err) console.log(err, err.stack);
    else {
       parent.handleMessage(request, function(response) {
           parent.sendMessage(JSON.stringify(response), function() {
               console.log("response sent");
               var params = { ReceiptHandle:request.Messages[0].ReceiptHandle };
               parent.deleteMessage(params, function() {
                   parent.waitForMessage();
               });
           });
       });
    }
});

如您所见,这段代码比较混乱,有四层嵌套的回调函数。

使用Q库,我已经找出了解决方法,您需要像下面这样开始:

Q.nfcall(this.connection.receiveMessage, params)
    .then(function(err, request) {
        return(Q.nfcall(this.handleMessage(request));
    })
    .then(function(response)) { 
        return(Q.nfcall(this.sendMessage(JSON.stringify(response))));
    } ...

等等……这好像不太对。首先,我需要在链中的每个函数上调用Q.nfcall吗?此外,我如何避免使用回调时出现的“this”作用域问题?我是否正确地使用了promises?

2个回答

2

我曾经也遇到过类似的问题,后来我发现这是因为 Q 库的原因。在我看来,Q 库的API混乱不堪,使用起来非常麻烦,而且提供的简单示例很少。我建议尝试其他任何库,但我确实推荐Bluebird。使用 Bluebird,您可以执行以下操作:

var Promise = require('bluebird');
var parent = this;
Promise.promisifyAll(parent, { suffix: "P" });
parent.receiveMessageP(params)
  .then(function (request) {
    return [request, parent.handleMessageP(request)];
  })
  .spread(function (request, response) {
    return [request, parent.sendMessageP(JSON.stringify(response))];
  })
  .spread(function (request) {
    console.log("response sent");
    var params = { ReceiptHandle: request.Messages[0].ReceiptHandle };
    return parent.deleteMessageP(params);
  })
  .then(function () {
    parent.waitForMessage();
  })
  .catch(function (err) {
    console.log(err, err.stack);
  });

如果你不喜欢返回数组并使用 .spread 的方式,你可以在外部作用域中使用映射对象。

var Promise = require('bluebird');
var parent  = this;
Promise.promisifyAll(parent, { suffix: "P" });
var cache = {};
parent.receiveMessageP(params)
  .then(function (request) {
    cache.request = request;
    return parent.handleMessageP(request);
  })
  .then(function (response) {
    return parent.sendMessageP(JSON.stringify(response));
  })
  .then(function () {
    console.log("response sent");
    var params = { ReceiptHandle: cache.request.Messages[0].ReceiptHandle };
    return parent.deleteMessageP(params);
  })
  .then(function () {
    parent.waitForMessage();
  })
  .catch(function (err) {
    console.log(err, err.stack);
  });

如果您需要在链中稍后访问先前解析的变量,可以将它们简单地添加到缓存对象中以便轻松访问。有时,如果您有很多这样的变量,这种方法更加清晰易读。在大多数情况下,我通常更喜欢第一个示例,只是为了避免污染父作用域并潜在地保留应该被处理的引用。
请注意,您可以在Q中执行类似于"promisifyAll"的操作,但Bluebird的性能更高且更直观。
如果您的回调函数不符合典型的节点样式签名function (err, successValue)(其中一些似乎不符合,这意味着"promisifyAll"无法在其上工作),则可以在Bluebird中定义自定义“promisifier”。或者修改回调API以符合节点样式回调
https://github.com/petkaantonov/bluebird/blob/master/API.md#option-promisifier

请注意,handleMessagesendMessagedeleteMessage实际上并不遵循节点回调约定。 - Bergi
嗯,如果这些是他自己的方法,最好将它们promisify :-) - Bergi
不,他没有说它不起作用。再读一遍。他问如何避免每次都使用nfcall。 - Chev
1
哎呀,你说得对。但他的代码显然是不能工作的 :-) - Bergi
1
不,我的应用程序中没有自定义方法。我忽略了err对象,因为我不知道它是必需的,并且我试图提供一个简化的示例,避免所有混乱。 - Mike
显示剩余3条评论

1
承诺确实可以让您取消回调嵌套,但是内联应用nfcall很麻烦。您可以将nbind作为修饰符应用于原始函数,以便您可以将其用作返回承诺的函数来构建链式结构:
obj.receiveMessage = Q.nbind(obj.receiveMessage, obj);
obj.deleteMessage = ...

现在它的阅读效果会更好:

this.receiveMessage(params)
  .then(function(request) {
    return parent.handleMessage(request);
  })
  .then(function(response) {
    var params = {ReceiptHandle: request.Messages[0].ReceiptHandle};
    return parent.deleteMessage(params);
  })
  .then(parent.waitForMessage)
  .catch(function(err) {
    console.log(err, err.stack);
  });

请注意,handleMessagesendMessagedeleteMessage实际上并不遵循节点回调约定。 - Bergi
还需要注意一点,第二个then处理程序中对request.Messages[0]的调用会导致Cannot call Messages[0] of undefined。你需要通过从第一个和第二个处理程序返回数组,并使用spread而不是then将数组结果扩展到链中的下一个处理程序的多个参数中来传递request。请参见我的答案中的示例;Q也具有相同的能力。 - Chev
他们为什么不遵循节点回调约定?是因为它们必须都带有错误和数据吗?如果是这样的话,实际上它们确实有,但是出于简单起见,我没有在这里放置错误对象。我不知道那是节点的要求。 - Mike

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