【问题标题】:I'm just not getting ES6 "promises"我只是没有得到 ES6 的“承诺”
【发布时间】:2016-08-22 12:53:47
【问题描述】:

我正在编写一个 Node.js 应用程序:

  1. 接受文件(来自 HTTP“POST”),然后
  2. 将文件写入 Box.com 存储库。

这个从 HTTP“POST”消息中读取文件数据的 Node.js 代码完美运行:

 // ORIGINAL (non-Promise; parse only)
 app.post('/upload', function(req,res) {
   console.log('/upload...');

   var form = new multiparty.Form ();
   form.parse(req, function(err, fields, files) {
     res.writeHead(200, {'content-type': 'text/plain'});
     res.write('received upload:\n\n');
     res.end(util.inspect({fields: fields, files: files}));
 });

问题是读取文件数据只是我需要做的几件事中的第一个,所有这些都涉及异步回调。所以我尝试使用promises 来序列化调用(根据需要优雅地处理错误):

var Promise = require('bluebird');
  ...
  // IDEAL SOLUTION (using "promises")
  var data = {};
  try {
    parsePostMsg(req, data))
    .then(sendAck(res, data))
    .then(writeTempFile())
    .then(sendToBox())
    .then(deleteTempFile());
  } catch (e) {
    console.log("app.post(/upload) ERROR", e);
    deleteTempFile();
  }

问题:

第一个函数parsePostMsg() 本身有一个回调。永远不会被调用:

function parsePostMsg(req, data) {
  console.log("parsePostMsg...");
  return new Promise(function(resolve, reject) {
    var form = new multiparty.Form ();
    form.parse(req, function(err, fields, files) {
      data.fields = fields; // <-- This never gets called, so fields & files
      data.files = files;   //     never get initialized!
    });
  });
}

问:我如何正确地 1) 创建承诺,然后 2) 调用 parsePostMsg() 以便 3) form.parse() 正确地沿链被调用?

问:我是否在正确的地方正确地创建了“承诺”????

问:resolve()reject() 呢?它们适合哪里?

================================================ ========================

附录:我尝试了很多东西,到目前为止都没有任何效果。

目标:让这些函数(及其相关的回调,如果适用)按此顺序运行:

  1. parsePostMsg(req, data) // 等待“解析表单数据”回调完成
  2. sendAck(res, data) // 同步
  3. writeTempFile(data) // 构建 HTTP 消息,发送,等待远程服务器响应
  4. deleteTempFile(data) // 一切完成后清理

这是一个失败的例子:

/*
 * If the "timeout" values are the same (e.g. "10"), everything works fine.
 *
 * But if the timeout values are *different*, the order gets scrambled:
 *     Invoking parsePostMsg...
 *     ... OK ...
 *     Done: data= {}
 *     writeTemp@setting data.temp { temp: 'foo' }
 *     sendAck@acknowledging {} { temp: 'foo' }
 *     deleteTempFile@callback: data.temp was  foo
 *     parsePostMsg@setting data.{fields, files} {} { temp: undefined, fields: [], files: [] }
 */
var Promise = require('bluebird');

var req = {}, res = {}, data = {};
var temp;

function parsePostMsg(req, data) {
  console.log("parsePostMsg...");
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      data.fields = [];
      data.files = [];
      console.log("parsePostMsg@setting data.{fields, files}", req, data);
      resolve();
    }, 35);
  });
}

function sendAck(req, data) {
  console.log("sendAck...");
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("sendAck@acknowledging", req, data);
      resolve();
    }, 5);
  });
}

function writeTempFile(data) {
  console.log("writeTemp...");
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      data.temp = "foo";
      console.log("writeTemp@setting data.temp", data);
      resolve();
    }, 2);
  });
}

function deleteTempFile(data) {
  console.log("deleteTemp...");
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      console.log("deleteTempFile@callback: data.temp was ", data.temp);
      data.temp = undefined;
      resolve();
    }, 15);
  });
}

console.log("Invoking parsePostMsg...");
parsePostMsg(req, data)
  .then(sendAck(res, data))
  .then(writeTempFile(data))
  .then(deleteTempFile(data));
console.log("Done: data=", data);

【问题讨论】:

标签: javascript node.js callback promise


【解决方案1】:

您失败的示例不起作用,因为您没有正确链接。

让我们看看这里的代码:

parsePostMsg(req, data)
    .then(sendAck(res, data))
    .then(writeTempFile(data))
    .then(deleteTempFile(data))
    // This console.log will execute way before
    // the promise is resolved
    console.log("Done: data=", data);

发生的情况是 parsePostMsg() 被正确调用,但是之后的所有内容都将在 promise 解决之前执行。这是因为您实际上是在立即执行这些函数,然后这些执行的输出就是 Promise 在解析时将尝试使用的内容。这就是为什么如果您将 parsePostMsg() 中的超时设置为几秒钟,该函数的输出将最后记录。

所以这应该是这样的:

parsePostMsg(req, data)
    .then(sendAck)
    .then(writeTempFile)
    .then(deleteTempFile)
    .then(function () {
        // Now the console.log will log when everything is done.
        console.log("Done: data=", data);
    });

在这里,您告诉 Promise,它应该在 Promise 解决时执行函数。但要做到这一点,我们必须以正确的方式建立承诺链。要将这些方法链接在一起,我们必须在前一个函数中返回要在函数中使用的值。例如,我们必须在 parsePostMsg() 函数中返回 sendAck() 函数的参数。

让我们编写 parsePostMsg 以便它返回链中所需的所有参数。

function parsePostMsg(req, res, data) {
    console.log("parsePostMsg...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            data.fields = [];
            data.files = [];
            console.log("parsePostMsg@setting data.{fields, files}", req, data);

            // Here we pass all the arguments into the resolve method
            // This means that the following then() call will receive
            // These argument. Take note that this is an array.
            resolve([req, res, data]);
        }, 3000);
    });
}

现在我们已经更改了 parsePostMsg 以便它将所有参数传递到链中。让我们以相同的方式更改其他方法。

function sendAck(req, res, data) {
    console.log("sendAck...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("sendAck@acknowledging", res, data);
            resolve([req, res, data]);
        }, 3000);
    });
}

function writeTempFile(req, res, data) {
    console.log("writeTemp...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            data.temp = "foo";
            console.log("writeTemp@setting data.temp", data);
            resolve([req, res, data]);
        }, 3000);
    });
}

function deleteTempFile(req, res, data) {
    console.log("deleteTemp...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("deleteTempFile@callback: data.temp was ", data.temp);
            data.temp = undefined;
            resolve([req, res, data]);
        }, 3000);
    });
}

现在请注意,所有函数都接受 3 个参数,但我们使用数组调用 resolve 方法。如果我们简单地使用 .then() ,这将无法正常工作,但是 bluebird 提供了一个特殊的辅助方法,称为 .spread() ,它会破坏数组并使用数组的所有成员调用该方法。请注意,如果您使用的是 ES2015,则可以使用 ES2015 破坏而不是使用 .spread()。

所以使用 .spread() 代码应该是这样的:

parsePostMsg(req, res, data)
    .spread(sendAck)
    .spread(writeTempFile)
    .spread(deleteTempFile)
    .spread(function (req, res, data) {
        // Now the console.log will log when everything is done.
        console.log("Done: data=", data);
    });

这是您提供的正常工作的失败示例:

var Promise = require('bluebird');

var req = {},
    res = {},
    data = {};
var temp;

function parsePostMsg(req, res, data) {
    console.log("parsePostMsg...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            data.fields = [];
            data.files = [];
            console.log("parsePostMsg@setting data.{fields, files}", req, data);
            resolve([req, res, data]);
        }, 3000);
    });
}

function sendAck(req, res, data) {
    console.log("sendAck...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("sendAck@acknowledging", res, data);
            resolve([req, res, data]);
        }, 3000);
    });
}

function writeTempFile(req, res, data) {
    console.log("writeTemp...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            data.temp = "foo";
            console.log("writeTemp@setting data.temp", data);
            resolve([req, res, data]);
        }, 3000);
    });
}

function deleteTempFile(req, res, data) {
    console.log("deleteTemp...");
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("deleteTempFile@callback: data.temp was ", data.temp);
            data.temp = undefined;
            resolve([req, res, data]);
        }, 3000);
    });
}

console.log("Invoking parsePostMsg...");

parsePostMsg(req, res, data)
    .spread(sendAck)
    .spread(writeTempFile)
    .spread(deleteTempFile)
    .spread(function (req, res, data) {
        console.log("Done: data=", data);
    })
    .catch(function(err) {
        // Always put a .catch() at the end of your promise chains, ALWAYS,
    // it is literally the ultimate method to handle promise errors.
        console.warn(err);
    });

【讨论】:

  • 太棒了——谢谢!在(关键点!)我错过了:1)使用“spread()”与“.then()”,2)使用“resolve”将结果从一个“thenable”函数传递到下一个(消除任何需要全局变量),3)通过仅传递函数名称来调用函数的重要性 - WITHOUT "()" 和 WITHOUT 参数列表。它就像一个魅力。谢谢。
  • 您知道,除了我的经验中的特殊情况外,通常不需要使用 new Promise() 。我建议使用 Promise.try() 来启动 Promise 链,或者如果您正在使用使用回调的模块,请按照 @JosephTheDreamer 的建议使用 bluebird Promisify。大多数情况下,您可以简单地在 Promise 函数中正常返回一个值,该值将沿链传递。
  • 点很好。实际上,“Promise.promisify()”是我尝试过的(很多很多)事情之一。但它没有工作......因为 other 问题,你解释得很好。我现在将添加“promisify()” - 谢谢你的提醒。
【解决方案2】:

首先,我想提一下 Bluebird 有一个名为 promisify 的函数,它将基于回调的函数转换为返回承诺的函数。

但在引擎盖下...

创建承诺

您已经在正确的轨道上。 Promise 是具有 2 种状态(已解决和已拒绝)的对象。您需要做的就是定义何时它解决或拒绝。这就是 resolvereject 函数的用途。

function parsePostMsg(req, data) {

  // Return a promise
  return new Promise(function(resolve, reject) {
    var form = new multiparty.Form();

    // that callse your async, callback-ish function during construction
    form.parse(req, function(err, fields, files) {

      // that rejects when there's an error
      if(err) reject(err);

      // or resolves when everything goes well
      else resolve({ fields: fields, files: files });

    });
  });
}

调用 parsePostMsg()

要知道您的承诺异步操作何时解析或拒绝,承诺公开一个then 方法,该方法接受 2 个回调。第一个在承诺解决时执行,第二个在承诺被拒绝时执行。它们都传递了调用resolve/reject时使用的参数。

在这种情况下,我们预计当 Promise 被拒绝时来自 form.parse 的错误或当 Promise 解决时包含字段和文件的对象。

// parsePostMsg returns a promise where we hook a then
parsePostMsg(req, data).then(function(data){

  // The object you passed to `resolve` is `data` in here
  // data.fields
  // data.files

}, function(error){

  // The `err` you passed to `reject` is `error` in here

})

我是否在正确的地方正确地创建了“承诺”????

参考第一块

resolve() 和 reject() 呢?它们适合哪里?

参考第一块

【讨论】:

    【解决方案3】:

    我对 Promise 不熟悉,但您可能应该阅读这篇文章,https://promisesaplus.com/,我认为您的 try catch 也没用,因为 Promise 引发的任何异常都与该代码分开。

    resolve 函数是在 promise 完成时运行的函数,reject 是在它失败时运行的函数,reject 是你可能想要处理错误的地方。请记住,附加到承诺的所有内容都独立于任何周围的代码。

    【讨论】:

      【解决方案4】:

      其实你已经很接近了!您正在正确地创建承诺并使用.then 调用它。不幸的是,你的链中的下一步永远不会开始,因为你的承诺永远不会解决!

      resolvereject 是 Promise 的表达方式“这有效,继续下一步”或“这不起作用,抛出错误!”分别。在你的情况下,你可能想做这样的事情:

      function parsePostMsg(req, data) {
        console.log("parsePostMsg...");
        return new Promise(function(resolve, reject) {
          var form = new multiparty.Form ();
          form.parse(req, function(err, fields, files) {
            if (err) {
              reject(err)
            } else {
              data.fields = fields;
              data.files = files;
              resolve();
            }
          });
        });
      }
      

      注意,使用 Promise 模型,您可以在内部方法中使用 resolve 从外部方法“返回”!

      请注意,您需要确保链中的所有步骤都遵循此模式。我整理了一个简单的例子,使用setTimeout 模拟异步函数调用,向您展示它的样子:

      var data = {};
      var req, res;
      var temp;
      
      function parsePostMsg(req, data) {
        console.log("parsePostMsg...");
        return new Promise(function(resolve, reject) {
          setTimeout(function() {
            data.fields = [];
            data.files = [];
            resolve();
          }, 10);
        });
      }
      
      function sendAck(req, data) {
        console.log("sendAck...");
        return new Promise(function(resolve, reject) {
          setTimeout(function() {
            console.log("acknowleding", req, data);
            resolve();
          }, 10);
        });
      }
      
      function writeTempFile() {
        console.log("writeTemp...");
        return new Promise(function(resolve, reject) {
          setTimeout(function() {
            temp = "foo";
            resolve();
          }, 10);
        });
      }
      
      function deleteTempFile() {
        console.log("deleteTemp...");
        return new Promise(function(resolve, reject) {
          setTimeout(function() {
            console.log("temp was ", temp);
            temp = undefined;
            resolve();
          }, 10);
        });
      }
      
      parsePostMsg(req, data)
        .then(sendAck(res, data))
        .then(writeTempFile())
        .then(deleteTempFile());

      【讨论】:

      • 感谢您的出色回复……但仍然无法正常工作。如果我按原样运行您的示例,那就完美了。但是,如果我将“超时”值修改为随机值……回调顺序会被打乱。具有较短超时的回调首先运行;较长的超时持续。我试图强制“sendAck”等到 parsePostMsg 发出“done”信号,让“writeTempFile”等到“sendAck”完成,等等。 问:我怎样才能强制执行“order”?我怎样才能“序列化”这些调用??????
      猜你喜欢
      • 2022-01-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-01
      • 2016-03-19
      相关资源
      最近更新 更多