在node.js中嵌套的promises是否正常?

时间:2016-03-04 20:38:39

标签: javascript node.js promise

我在学习node.js的过程中遇到了两个星期的问题,就是如何使用node进行同步编程。我发现无论我如何尝试按顺序执行 ,我总是以嵌套的承诺结束。我发现有一些像Q这样的模块可以帮助保证链的承诺链。

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

当来自Java或C#等编程语言时,node.js肯定存在学习曲线。仍然存在的问题是如果promise.js中的promise链接是正常的(最佳实践)。

示例:

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

7 个答案:

答案 0 :(得分:86)

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

Promise为你提供了返回语句和错误抛出,你会因为继续传递样式而丢失。

您需要从异步函数返回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获取一系列承诺并在所有承诺解析后解析,如果有任何拒绝,则拒绝该阵列。这允许您同时执行异步代码而不是串行执行异步代码,并且仍然等待所有并发函数的结果。如果您对线程模型感到满意,请考虑生成线程然后加入。

示例:

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方法。)
拒绝将拒绝承诺(这意味着不会调用任何链式then方法,但会调用第一个链式catch方法并出现错误)

我离开了setTimeout以保留您的计划行为,但这可能是不必要的。

答案 1 :(得分:8)

使用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都已解析时解析,或者拒绝第一个传递的拒绝承诺的原因。

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对象。如果该值是一个值(即具有then方法),则返回的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

答案 2 :(得分: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本身就是一个非常强大的功能,我认为它是一种将promises和库混合起来的反模式,如async。

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

  3.   

    '包装不符合标准的回调API   惯例。像setTimeout:'

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

    对于setTimeout示例..创建一个' deferred' promise ...解决setTimeout中的promise,然后在setTimeout之外返回promise。这似乎有点不直观。 看看这个例子,我回答了另一个问题。 Q.js promise with node. Missing error handler on `socket`. TypeError: Cannot call method 'then' of undefined

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

    1. Promise.all 假设您正在多次调用异步返回的服务。 如果他们不相互依赖,您可以同时拨打电话。
    2. 将函数调用作为数组传递。 Promise.all([promiseReturningCall1,promiseReturningCall2,promiseReturningCall3]);

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

答案 3 :(得分:2)

我刚刚回答了一个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());

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

如果您打算为此目的使用协同程序,我建议您查看co library

希望这有帮助。

PS 不确定您为什么以这种方式使用setTimeout。具体等待750毫秒的重点是什么?

答案 4 :(得分:1)

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

以下是关于它的精彩博文:Flattening Promise Chains。它使用Angular但您可以忽略它并查看promise的深层嵌套如何变成链。

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

答案 5 :(得分: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);
});

}); });

答案 6 :(得分: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 )
    } );