嵌套的Promise在node.js中正常吗?


70

我在学习node.js时一直困扰着两个星期的问题是如何使用node进行同步编程。我发现,无论我如何尝试按顺序进行操作,我总是以嵌套的诺言告终。我发现存在诸如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);
        });
    });
});

Answers:


107

不,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都解决后解决,如果任何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代替回调。

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

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


谢谢,这对我很有帮助。
Grim

@TateThurston原谅我太菜鸟了。我有一个问题,连锁承诺(如您的第一个示例)是否按顺序执行?因此,假设第一个承诺花了大约1秒钟来完成异步请求,被第一个约束的第二个承诺.then要等到第一个承诺完成后才能执行,对吗?第三承诺等是否相同?
JohnnyQ '17年

1
我是否理解正确,如果您有多个异步调用依赖于另一个异步调用的结果,您仍然会有嵌套的Promise?确实没有办法避免这种情况。
Levi Fuller

@LeviFuller您仍然可以保持平坦。如果您有特定情况,我们很乐意回答。
塔特·瑟斯顿

@TateThurston太棒了。例如,如果呼叫非常依赖于上次呼叫的结果,我该如何展平这样的内容?getOrganization().then(orgId => getFirstEnvironment(orgId).then(environmentId => getAppList.then(appList => console.log(appList)))).catch(err => console.log(err));
Levi Fuller

9

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

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

Promise.all(iterable)方法返回一个promise,当可迭代参数中的所有promise都已解决时,该promise会解决;或者以第一个传递的promise拒绝为由拒绝。

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,并采用其最终状态;否则,返回的承诺将被履行。

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值完全一样(即它是一个值数组)?
Grim

也可以说我在对象内部的函数中使用async.series(),并且需要在该函数内部链接promise。我会返回async.series()还是什么都不返回给调用者?
Grim

5
这里没有理由使用该async库来避免嵌套。OP可以链接其诺言调用而无需嵌套。
jfriend00

@ jfriend00什么时候使用异步库?
Grim

2
@CharlesSexton-一旦您学习了如何使用Promise并习惯了异步操作的增强错误处理和对异步操作的一般管理的简易性,您将永远不想在没有它的情况下编写异步代码,但我找不到任何异步代码像Bluebird Promise库(这就是我所使用的)之类的库提供的商品,不能像以前那样容易或更轻松地完成。我不使用异步库,因为一旦我了解了诺言,就没有找到理由。
jfriend00

3

我删除了不必要的嵌套。不良使用'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);

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

  1. 在处理诺言时,无需使用异步库。Promises本身就是一个非常强大的工具,我认为这是一种将Promise和类似异步的库混合使用的反模式。

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

'当包装不遵循标准约定的回调API时。就像setTimeout:

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

对于setTimeout示例..创建一个“递延” promise ...在setTimeout中解析promise,然后在setTimeout外返回promise。这似乎有点不直观。看这个例子,我回答了另一个问题。 Q.js承诺与节点。套接字上缺少错误处理程序。TypeError:无法调用未定义的方法“ then”

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

  1. Promise.all可以说您正在多次调用异步返回的服务。如果它们彼此不依赖,则可以同时拨打电话。

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

  1. 最后,在最后添加一个catch块,以确保捕获任何错误。这将捕获链中任何地方的任何异常。

你从哪里来Promise.pending()的?这不是我所知道的标准Promise方法。为什么不只使用标准的Promise构造函数?另外,在此过程中插入一个任意延迟(我知道这不是您的想法,而是来自OP),闻起来像是黑客,这可能不是正确的编码方式。
jfriend00

Promise.pending是一个蓝鸟语法。我同意setTimeout用法看起来很有趣。我将其保留下来以解释手动Promise创建。
Arun Sivasankaran '16

然后两点。1)您应该在回答中说您正在依赖Bluebird(这也是我首选的Promise库),因为它超出了标准Promise。2)如果您使用的是Bluebird,则最好使用Promise.delay()代替Promise.pending()setTimeout()
jfriend00

我更新了答案以提及“蓝鸟”。Promise.delay实际上是添加手动延迟的更好方法。
Arun Sivasankaran '16

有趣的是,我Promise.pending()在Bluebird API文档页面中没有看到。是旧的还是新的API?
jfriend00

2

我刚刚回答了一个类似的问题,在其中我解释了一种使用生成器以很好的方式展平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());

但是,这只是在摸索使用协程控制承诺流的可能性。阅读我上面链接的答案,该答案演示了此技术的其他一些优点和功能。

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

希望这可以帮助。

PS不确定为什么要setTimeout以这种方式使用。具体等待750 ms的意义是什么?


setTimeout用于等待浏览器呈现新页面然后执行这些操作,Node.js在进行自动化测试时确实非常快。我需要做更多的研究,但这是一个很好的答案,可能是最好的答案。
Grim

@CharlesSexton好吧,应该有一些方法可以包装回调/承诺。我不确定您使用的是哪个测试库,但是等待诸如此类的任意数量是很愚蠢的。
谢谢您

我当时使用的是mocha / Chai,但我只是在编写带有承诺的练习脚本。您为自动化测试推荐什么测试库?我认为Promise是比回调更好的设计方法。我可能可以将其包装在回调中,但是我必须使用它才能看到。
Grim

mocha / chai没有类似.insertUserName或的方法.clickAddEmployee
谢谢您

insertUserName和clickAddEmployee来自页面对象。我实际上没有进行任何检查,但可以添加一个用户,然后在添加后在列表中搜索该用户
Grim

1

下一步是从嵌套到链接。您需要意识到,每个承诺都是孤立的承诺,可以与父承诺链接在一起。换句话说,您可以使对链的承诺变平。每个承诺结果都可以传递给下一个。

这是一篇很棒的博客文章:扁平化承诺链。它使用了Angular,但是您可以忽略它,而看看深层的promises如何变成一条链。

另一个很好的答案就在StackOverflow上:了解javascript承诺;堆栈和链接


0

是的,就像@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 )
    } );

0

您可以像这样链接承诺:

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);
        });
    });
});

1
我为多重回报感到困惑。你有.then(function(){return statement}); 像这样使用时,返回的值将流向何处?在同步编程中,返回返回给调用者。来电者是什么?
Grim
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.