繁体   English   中英

Node.js 中的嵌套 promise 是否正常?

[英]Are nested promises normal in Node.js?

在学习 Node.js 时,我已经苦苦挣扎了两周的问题是如何使用 node 进行同步编程。 我发现无论我如何尝试按顺序做事,我总是以嵌套的 promises 告终。 我发现有诸如 Q 之类的模块可以在可维护性方面帮助承诺链。

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

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

例子:

driver.get('https://website.example/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);
        });
    });
});

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

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

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

这是一个例子:

driver.get('https://website.example/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方法)。 Reject将拒绝承诺(这意味着不会调用任何链接的then方法,但将调用第一个链接的catch方法并出现错误)。

我将您的setTimeout保留在适当的位置以保留您的程序行为,但这可能是不必要的。

使用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

我刚刚回答了一个类似的问题,我解释了一种使用生成器以一种很好的方式扁平化 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.example/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.example/login')
  yield loginPage.login('company.admin', 'password');
  yield employeePage.clickAddEmployee();
  setTimeout(() => coro(createEmployee()), 750);
}

coro(login());

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

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

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

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

var employeePage;

driver.get('https://website.example/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. 使用 Promise 时无需使用异步库。 Promise 本身就非常强大,我认为混合 Promise 和诸如 async 之类的库是一种反模式。

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

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

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

对于 setTimeout 示例..创建一个“延迟”承诺...解决 setTimeout 内的承诺,然后在 setTimeout 之外返回承诺。 这可能看起来有点不直观。 看这个例子,我回答了另一个问题。 Q.js 承诺与节点。 `socket` 上缺少错误处理程序。 TypeError:无法调用未定义的方法“then”

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

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

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

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

由于这篇文章是谷歌上“嵌套承诺”的最佳结果,并且在我早期从 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));
    }

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

因此,它的网络是外部函数定义了 promise 结构并启动异步调用,但随后立即完成它们的块,因为 JS 首先是一种同步语言; 所以请注意并假设所有异步调用甚至在调用它的块完成之后开始

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

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

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

另一个很好的答案就在 StackOverflow 上: Understanding javascript promises; 堆栈和链接

你可以像这样链接承诺:

driver.get('https://website.example/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);
        });
    });
});

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

这是一个例子:

driver
    .get( 'https://website.example/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 )
    } );

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM