node.js 中的嵌套承诺是不是正常?

Posted

技术标签:

【中文标题】node.js 中的嵌套承诺是不是正常?【英文标题】:Are nested promises normal in node.js?node.js 中的嵌套承诺是否正常? 【发布时间】:2016-06-18 18:03:03 【问题描述】:

我在学习 node.js 的过程中一直在努力解决的问题是如何使用 node.js 进行同步编程。我发现无论我如何尝试按顺序做事,我总是以嵌套的 promises 告终。我发现有诸如 Q 之类的模块可以在可维护性方面帮助实现承诺链。

我在做研究时不明白的是Promise.all()Promise.resolve()Promise.reject()Promise.reject 的名称几乎可以自我解释,但是在编写应用程序时,我对如何在不破坏应用程序行为的情况下将任何这些包含在函数或对象中感到困惑。

从 Java 或 C# 等编程语言学习 node.js 肯定有一个学习曲线。仍然存在的问题是,promise 链在 node.js 中是否正常(最佳实践)。

例子:

driver.get('https://website.com/login').then(function () 
    loginPage.login('company.admin', 'password').then(function () 
        var employeePage = new EmployeePage(driver.getDriver());

        employeePage.clickAddEmployee().then(function() 
            setTimeout(function() 
                var addEmployeeForm = new AddEmployeeForm(driver.getDriver());

                addEmployeeForm.insertUserName(employee.username).then(function() 
                    addEmployeeForm.insertFirstName(employee.firstName).then(function() 
                        addEmployeeForm.insertLastName(employee.lastName).then(function() 
                            addEmployeeForm.clickCreateEmployee().then(function() 
                                employeePage.searchEmployee(employee);
                            );
                        );
                    );
                );
            , 750);
        );
    );
);

【问题讨论】:

Here is a very similar question I asked a while back 【参考方案1】:

不,Promises 的一大优势是您可以保持异步代码线性而不是嵌套(来自延续传递风格的回调地狱)。

Promise 会为您提供返回语句和错误抛出,而您会因延续传递风格而失去这些。

您需要从您的异步函数中返回承诺,以便您可以链接返回的值。

这是一个例子:

driver.get('https://website.com/login')
  .then(function() 
    return loginPage.login('company.admin', 'password')
  )
  .then(function() 
    var employeePage = new EmployeePage(driver.getDriver());
    return employeePage.clickAddEmployee();
  )
  .then(function() 
    setTimeout(function() 
      var addEmployeeForm = new AddEmployeeForm(driver.getDriver());

      addEmployeeForm.insertUserName(employee.username)
        .then(function() 
          return addEmployeeForm.insertFirstName(employee.firstName)
        )
        .then(function() 
          return addEmployeeForm.insertLastName(employee.lastName)
        )
        .then(function() 
          return addEmployeeForm.clickCreateEmployee()
        )
        .then(function() 
          return employeePage.searchEmployee(employee)
        );
    , 750);
);

Promise.all 接受一个 promise 数组,并在所有 promise 解析后解析,如果有任何一个被拒绝,则该数组被拒绝。这允许您并发而不是串行执行异步代码,并且仍然等待所有并发函数的结果。如果您对线程模型感到满意,请考虑生成线程然后加入。

例子:

addEmployeeForm.insertUserName(employee.username)
    .then(function() 
        // these two functions will be invoked immediately and resolve concurrently
        return Promise.all([
            addEmployeeForm.insertFirstName(employee.firstName),
            addEmployeeForm.insertLastName(employee.lastName)
        ])
    )
    // this will be invoked after both insertFirstName and insertLastName have succeeded
    .then(function() 
        return addEmployeeForm.clickCreateEmployee()
    )
    .then(function() 
        return employeePage.searchEmployee(employee)
    )
    // if an error arises anywhere in the chain this function will be invoked
    .catch(function(err)
        console.log(err)
    );

Promise.resolve()Promise.reject() 是创建Promise 时使用的方法。它们用于使用回调包装异步函数,以便您可以使用 Promises 而不是回调。

Resolve 将解决/履行承诺(这意味着将使用结果值调用链接的 then 方法)。Reject 将拒绝承诺(这意味着不会调用任何链接的 then 方法,但会调用第一个链接的 catch 方法并出现错误)。

我保留了您的 setTimeout 以保留您的程序行为,但这可能是不必要的。

【讨论】:

谢谢,这对我帮助很大。 @TateThurston 原谅我太菜鸟了。我有一个问题,链式承诺(如您的第一个示例中)是否按顺序执行?所以假设第一个承诺大约需要 1 秒来完成异步请求,由.then 链接的第二个承诺在第一个完成之前不会被执行,对吗?第三个promise等等也一样吗? 如果你有多个异步调用依赖于另一个异步调用的结果,你仍然会有嵌套的承诺,我是否正确理解?真的没有办法避免。 @LeviFuller 你仍然可以保持平稳。如果您有具体案例,我很乐意为您解答。 @TateThurston 那太棒了。例如,如果非常调用依赖于先前调用的结果,我怎么能展平这样的东西? getOrganization().then(orgId => getFirstEnvironment(orgId).then(environmentId => getAppList.then(appList => console.log(appList)))).catch(err => console.log(err));【参考方案2】:

使用async 库并使用async.series 而不是看起来非常丑陋且难以调试/理解的嵌套链接。

async.series([
    methodOne,
    methodTwo
], function (err, results) 
    // Here, results is the value from each function
    console.log(results);
);

Promise.all(iterable) 方法返回一个当可迭代参数中的所有承诺都已解决时解决的承诺,或者以第一个被拒绝的承诺的原因拒绝。

var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = new Promise(function(resolve, reject) 
  setTimeout(resolve, 100, "foo");
); 

Promise.all([p1, p2, p3]).then(function(values)  
  console.log(values); // [3, 1337, "foo"] 
);

Promise.resolve(value) 方法返回一个使用给定值解析的 Promise 对象。如果该值是 thenable(即具有 then 方法),则返回的 promise 将“跟随”该 thenable,采用其最终状态;否则返回的 Promise 将用该值实现。

var p = Promise.resolve([1,2,3]);
p.then(function(v) 
  console.log(v[0]); // 1
);

https://developer.mozilla.org/en/docs/Web/javascript/Reference/Global_Objects/Promise/all

【讨论】:

到目前为止,您拥有使用 async.series 和 Promise.all 的最干净的示例。 async.series 的结果是否与 promise.all 的值完全一样,它是一个值数组? 还可以说我在对象内部的函数中使用 async.series() 并且我需要在该函数内部链接承诺。我会返回 async.series() 还是什么都不返回给调用者? 这里没有理由使用async 库来避免嵌套。 OP 可以在没有嵌套的情况下链接他们的 Promise 调用。 @jfriend00 你什么时候使用异步库? @CharlesSexton - 一旦你学会了如何使用 Promise 并习惯了异步操作的增强错误处理和异步操作的一般易用性,你将永远不想编写没有它的异步代码,我'还没有发现异步库提供的任何东西不能像 Bluebird Promise 库(这是我使用的)那样容易或更容易地完成。我不使用异步库,因为一旦我学会了 Promise,我就找不到理由。【参考方案3】:

我刚刚回答了similar question,我在其中解释了一种使用生成器以一种很好的方式扁平化 Promise 链的技术。该技术的灵感来自协程。

获取这段代码

Promise.prototype.bind = Promise.prototype.then;

const coro = g => 
  const next = x => 
    let done, value = g.next(x);
    return done ? value : value.bind(next);
  
  return next();
;

使用它,你可以将你的深度嵌套的 Promise 链变成这个

coro(function* () 
  yield driver.get('https://website.com/login')
  yield loginPage.login('company.admin', 'password');
  var employeePage = new EmployeePage(driver.getDriver());
  yield employeePage.clickAddEmployee();
  setTimeout(() => 
    coro(function* () 
      var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
      yield addEmployeeForm.insertUserName(employee.username);
      yield addEmployeeForm.insertFirstName(employee.firstName);
      yield addEmployeeForm.insertLastName(employee.lastName);
      yield addEmployeeForm.clickCreateEmployee();
      yield employeePage.searchEmployee(employee);
    ());
  , 750);
());

使用命名生成器,我们可以使它更加清晰

// don't forget to assign your free variables
// var driver = ...
// var loginPage = ...
// var employeePage = new EmployeePage(driver.getDriver());
// var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
// var employee = ...

function* createEmployee () 
  yield addEmployeeForm.insertUserName(employee.username);
  yield addEmployeeForm.insertFirstName(employee.firstName);
  yield addEmployeeForm.insertLastName(employee.lastName);
  yield addEmployeeForm.clickCreateEmployee();
  yield employeePage.searchEmployee(employee);


function* login () 
  yield driver.get('https://website.com/login')
  yield loginPage.login('company.admin', 'password');
  yield employeePage.clickAddEmployee();
  setTimeout(() => coro(createEmployee()), 750);


coro(login());

然而,这只是触及了使用协程来控制 Promise 流的可能性的皮毛。阅读我在上面链接的答案,该答案展示了该技术的其他一些优点和功能。

如果您确实打算为此目的使用协程,我鼓励您查看co library。

希望这会有所帮助。

PS不知道你为什么以这种方式使用setTimeout。具体等待750毫秒有什么意义?

【讨论】:

setTimeout 用于等待浏览器渲染一个新页面然后执行这些操作,Node.js 在进行自动化测试时非常快。我需要做更多的研究,但这是一个非常好的答案,可能是最好的答案。 @CharlesSexton 应该有某种方法可以围绕它包装回调/承诺。我不确定您使用的是什么测试库,但是像这样等待任意数量是愚蠢的。 我使用的是 mocha/Chai,但我只是在编写脚本来练习 Promise。你推荐什么测试库来进行自动化测试?我认为 promises 是一种比回调更好的设计方法。我可能可以将它包装在回调中,但我必须使用它才能看到。 mocha/chai 没有像 .insertUserName.clickAddEmployee 这样的方法。 insertUserName 和 clickAddEmployee 来自页面对象。我实际上并没有检查任何东西,但我可以添加一个用户,然后在添加后在列表中搜索该用户【参考方案4】:

我删除了不必要的嵌套。我将使用“bluebird”中的语法(我首选的 Promise 库) http://bluebirdjs.com/docs/api-reference.html

var employeePage;

driver.get('https://website.com/login').then(function() 
    return loginPage.login('company.admin', 'password');
).then(function() 
    employeePage = new EmployeePage(driver.getDriver());    
    return employeePage.clickAddEmployee();
).then(function () 
    var deferred = Promise.pending();
    setTimeout(deferred.resolve,750);
    return deferred.promise;
).then(function() 
    var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
    return Promise.all([addEmployeeForm.insertUserName(employee.username),
                        addEmployeeForm.insertFirstName(employee.firstName),
                        addEmployeeForm.insertLastName(employee.lastName)]);
).then(function() 
    return addEmployeeForm.clickCreateEmployee();
).then(function() 
    return employeePage.searchEmployee(employee);
).catch(console.log);

我修改了您的代码以包含所有问题的示例。

    使用 Promise 时无需使用异步库。 Promise 本身就非常强大,我认为混合 Promise 和异步库之类的库是一种反模式。

    通常您应该避免使用 var deferred = Promise.pending() 样式...除非

'包装不符合标准的回调API时 习俗。喜欢 setTimeout:'

https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns

对于 setTimeout 示例..创建一个“延迟”承诺...解决 setTimeout 内的承诺,然后在 setTimeout 外返回承诺。这可能看起来有点不直观。 看这个例子,我回答了另一个问题。 Q.js promise with node. Missing error handler on `socket`. TypeError: Cannot call method 'then' of undefined

通常,您可以使用 Promise.promisify(someFunction) 将回调类型函数转换为 Promise 返回函数。

    Promise.all 假设您正在对异步返回的服务进行多次调用。 如果它们不相互依赖,您可以同时拨打电话。

只需将函数调用作为数组传递。 Promise.all([promiseReturningCall1, promiseReturningCall2, promiseReturningCall3]);

    最后添加一个 catch 块到最后..以确保您捕获任何错误。这将捕获链中任何位置的任何异常。

【讨论】:

你从哪里得到Promise.pending()?这不是我所知道的标准 Promise 方法。为什么不直接使用标准的 Promise 构造函数?此外,在此过程中插入的任意延迟(我知道这不是您的想法,而是来自 OP)闻起来像是一种 hack,这可能不是编写此代码的正确方法。 Promise.pending 是一种蓝鸟语法。我同意 setTimeout 的用法看起来很有趣。我把它留下来解释手动创建 Promise。 然后两点。 1)您应该在回答中说您依赖 Bluebird(这也是我首选的承诺库),因为它超出了标准承诺。 2)如果您使用的是Bluebird,那么您不妨使用Promise.delay() 而不是Promise.pending()setTimeout() 我更新了我的答案,提到了“蓝鸟”。 Promise.delay 实际上是添加手动延迟的更好方法。 有趣的是,我在 Bluebird API 文档页面中没有看到 Promise.pending()。那是旧的还是新的 API?【参考方案5】:

下一步是从嵌套到链接。您需要意识到每个 Promise 都是一个孤立的 Promise,可以链接在父 Promise 中。换句话说,您可以将承诺扁平化为链。每个 promise 结果都可以传递给下一个。

这是一篇很棒的博客文章:Flattening Promise Chains。它使用 Angular,但你可以忽略这一点,看看如何将 promise 的深层嵌套变成一个链。

另一个很好的答案就在 *** 上:Understanding javascript promises; stacks and chaining。

【讨论】:

【参考方案6】:

由于这篇文章是 Google 上“嵌套承诺”的最高结果,并且在我早期从 C# 背景学习 node.js 时一直在努力解决承诺,我想我会发布一些可以帮助其他人制作的东西类似的过渡/演变。

Tate 投票赞成的答案是完全正确的,因为它确实强制了一个序列,但对于大多数 .NET 或 Java 开发人员来说,问题是我们只是不习惯在同步语言中进行这么多的异步操作.您必须非常了解什么是异步,因为外部块会在任何异步操作之前继续并完成。

为了说明,这里有一些代码(完整的嵌套和两个错误!)我在使用 'pg-promise' 学习承诺时遇到了困难:

            exports.create = async function createMeet(thingJson, res, next) 
    let conn;
    if (helpers.isDate(helpers.isStringDate(thingJson.ThingDate)))
        db.connect()
            .then(obj => 
                conn = obj;
                conn.proc('spCreateThing',[
                    thingJson.ThingName,
                    thingJson.ThingDescription,
                    thingJson.ThingDate])
                    .then(data => 
                        res.status(201).json(data);
                        res.send();
                    )
                    .catch(error =>
                        console.error("Error creating a Thing via spCreateThing(" + thingJson + "): " + error);
                        next(createError(500, "Failed to create a Thing!"));
                    )
                    .finally(()  => 
                        conn.done(); //appropriate time to close the connection
                    );
                )
            .catch(error =>
                console.error("Error establishing postgres database connection: " + error);
                next(createError(500, "Error establishing postgres database connection: " + error));
            )
            .finally(()  =>  //this finally block will execute before the async actions fired in first .then() complete/start
                    conn.done(); //so this would close the connection before conn.proc() has completed/started
            );
        res.send(); //this will execute immediately following .connect() BEFORE any of the chained promise results,
        // thus sending a response before we've even figured out if the connection was successful and started the proc 
     else 
        console.error("Attempt to create a Thing without valid date: " + thingJson.ThingDate);
        next(createError(400, "Must specify a valid date: " + thingJson.ThingDate));
    

最重要的是,调用此函数的代码(即路由处理程序)将在数据库连接过程开始之前完成。

因此,它的核心是外部函数定义承诺结构并启动异步调用,然后立即完成它们的块因为 JS 首先是一种同步语言;所以请注意并假设所有异步调用都不会开始,直到调用它的块完成之后

我知道这对职业 JS 开发人员来说是显而易见的(现在对我来说也是如此),但我希望这真的能帮助其他刚接触这些概念的人。

【讨论】:

一方面,您应该使用async/await 语法,这要简单得多。另一方面,您不需要使用 pg-promise 调用 connect。您的数据库代码可以减少到只有一行 - await db.proc(...) 就是这样。【参考方案7】:

是的,就像@TateThurston 说的那样,我们把它们锁起来。 当你使用 es6 箭头函数时,它更美观?

这是一个例子:

driver
    .get( 'https://website.com/login' )
    .then( () => loginPage.login( 'company.admin', 'password' ) )
    .then( () => new EmployeePage( driver.getDriver() ).clickAddEmployee() )
    .then( () => 
        setTimeout( () => 
            new AddEmployeeForm( driver.getDriver() )
                .insertUserName( employee.username )
                .then( () => addEmployeeForm.insertFirstName( employee.firstName ) )
                .then( () => addEmployeeForm.insertLastName( employee.lastName ) )
                .then( () => addEmployeeForm.clickCreateEmployee() )
                .then( () => employeePage.searchEmployee( employee ) );
        , 750 )
     );

【讨论】:

【参考方案8】:

你可以像这样链接承诺:

driver.get('https://website.com/login').then(function () 
    return loginPage.login('company.admin', 'password')
).then(function () 
    var employeePage = new EmployeePage(driver.getDriver());

    return employeePage.clickAddEmployee().then(function() 
        setTimeout(function() 
            var addEmployeeForm = new AddEmployeeForm(driver.getDriver());
        return addEmployeeForm.insertUserName(employee.username).then(function() 
                retun addEmployeeForm.insertFirstName(employee.firstName)
         ).then(function() 
                return addEmployeeForm.insertLastName(employee.lastName)
         ).then(function() 
             return addEmployeeForm.clickCreateEmployee()
         ).then(function () 
             retrun employeePage.searchEmployee(employee);
        ), 750);
        );
    );
);

【讨论】:

我对多次退货感到困惑。你有 .then(function() return statement );当它像那样使用时,返回的值会去哪里,或者在这种情况下是承诺?在同步编程中,返回返回给调用者。来电者是什么?

以上是关于node.js 中的嵌套承诺是不是正常?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 node.js 中的特定持续时间后强制解决承诺? [复制]

如何遍历 node.js 中的一系列承诺?

Node.js承诺中的readline

如何在 node.js 中解决可变数量的承诺

Node.js 承诺会间歇性地失败,即使在处理时也是如此

Node.js:何时使用 Promises 与 Callbacks