避免在 javascript 中循环多次返回 - async / await 以解决回调金字塔或回调地狱,

Posted

技术标签:

【中文标题】避免在 javascript 中循环多次返回 - async / await 以解决回调金字塔或回调地狱,【英文标题】:avoid multiple returns looped in javascript - async / await to solve callback pyramid or callback hell, 【发布时间】:2018-02-20 08:48:26 【问题描述】:

我有这段代码,有很多返回块,例如 SignUp()

connectors.js

  const connectors = 
      Auth: 
        signUp(args) 
          return new Promise((resolve, reject) => 
            // Validate the data
            if (!args.email) 
              return reject(
                code: 'email.empty',
                message: 'Email is empty.'
              );
             else if (!isEmail(args.email)) 
              return reject(
                code: 'email.invalid',
                message: 'You have to provide a valid email.'
              );
            

            if (!args.password) 
              return reject(
                code: 'password.empty',
                message: 'You have to provide a password.'
              );
            

            return encryptPassword(args.password, (err, hash) => 
              if (err) 
                return reject(new Error('The password could not be hashed.'));
              

              return User.create(Object.assign(args,  password: hash ))
                .then((user) => 
                  resolve(createToken( id: user._id, email: user.email ));
                )
                .catch((err2) => 
                  if (err2.code === 11000) 
                    return reject(
                      code: 'user.exists',
                      message: 'There is already a user with this email.'
                    );
                  

                  return reject(err2);
                );
            );
          );
        ,
    ;

    module.exports = connectors;

然后调用此代码的另一个代码:

 const connectors = require('./connectors');

   CallsignUp(root, args) 
      const errors = [];

      return connectors.Auth.signUp(args)
        .then(token => (
          token,
          errors
        ))
        .catch((err) => 
          if (err.code && err.message) 
            errors.push(
              key: err.code,
              value: err.message
            );
            return  token: null, errors ;
          

          throw new Error(err);
        );
     

在 ES6 或 ES7 或 ES2017 中如何避免这种情况?

有:

  return()
   .then()
     return()
       .then

然后循环返回:

return()
   return()
       return()

来自 php 的这段代码看起来很疯狂,因为 return 函数返回函数,我不清楚,javascript 中的这种类型的块返回代码的名称是什么?调用再次返回更多代码的函数?

更新:

代码不是我的,完整源代码在

https://github.com/jferrettiboke/react-auth-app-example

我想通过例子来理解:

    return encryptPassword(args.password, (err, hash) => 
      if (err) 
        return reject(new Error('The password could not be hashed.'));
      

      return User.create(Object.assign(args,  password: hash ))
        .then((user) => 
        .catch((err2) => 

          return reject(err2);

/src/utils/auth.js(这里是加密密码)

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt-nodejs');
const config = require('../config');
exports.encryptPassword = (password, callback) => 
  // Generate a salt then run callback
  bcrypt.genSalt(10, (err, salt) => 
    if (err)  return callback(err); 

    // Hash (encrypt) our password using the salt
    return bcrypt.hash(password, salt, null, (err2, hash) => 
      if (err2)  return callback(err2); 
      return callback(null, hash);
    );
  );
;

有3个返回调用函数和返回值和函数? OOP 从来都不是这样,@dashmud 建议的如何使用 Async/Await,我不想学习回调之类的老东西

【问题讨论】:

"return functions that return functions" - 当你说 return reject(...) 立即调用 reject() 函数然后返回任何 its 返回价值是。这在几乎所有现代语言中都很正常,并且与返回函数不同。 欢迎来到函数式编程——这是 PHP 最擅长的领域。 顺便说一句,我刚刚注意到一些让我有点担心的命名。看来您有一个名为encryptPassword() 的函数实际上是在散列它。加密与散列不同,这不仅仅是语义,这是存储敏感数据的一个巨大的安全问题。确保您的密码实际上是 hashed,这是一个没有密钥的单向函数,而不是加密,这是一个有密钥的双向函数,并将您的函数命名为这样,例如hashPassword(). 请阅读我的问题中的更新,代码不是我的,我只是尝试理解代码并寻找更新到 es7、回调、关于安全性和其他原因并不重要问题 仅供参考,async/await 是 ES2017(今年发布)的一部分,而不是 ES7(ES2016,去年发布)。 【参考方案1】:

此代码需要以多种方式重构。首先,你真的,真的不想在同一个逻辑流程中混合使用 Promise 和普通的异步回调。它弄得一团糟,破坏了 promise 的许多优点。然后,你有一个使用 new Promise() 内部的 Promise 的反模式。然后,您需要更多的嵌套(您可以改为链式)。

以下是我的建议:

function encryptPasswordPromise(pwd) 
    return new Promise((resolve, reject) => 
        encryptPassword(pwd, (err, hash) => 
            err ? reject(new Error("The password could not be hashed.")) : resolve(hash);
        );
    );


const connectors = 
    Auth: 
        signUp(args) 
            // Validate the data
            let err;
            if (!args.email) 
                err = code: 'email.empty', message: 'Email is empty.';
             else if (!isEmail(args.email)) 
                err = code: 'email.invalid', message: 'You have to provide a valid email.';
             else if (!args.password) 
                err = code: 'password.empty', message: 'You have to provide a password.';
            
            if (err) 
                return Promise.reject(err);
             else 
                return encryptPasswordPromise(args.password).then(hash => 
                    args.password = hash;
                    return User.create(args);
                ).then((user) => 
                    return createToken(id: user._id, email: user.email);
                ).catch(err2 => 
                    if (err2.code === 11000) 
                        throw new Error(code: 'user.exists', message: 'There is already a user with this email.');
                     else 
                        throw err2;
                    
                );
            
        
    
;

变更摘要:

    最初收集所有错误,并在一个地方为所有这些初始错误返回Promise.reject(err)。 Promisify encryptPassword() 这样您就不会将常规回调与 Promise 逻辑流混为一谈,这样您就可以正确地返回和传播错误。 使用return new Promise() 删除整个代码的包装,因为您的所有异步操作现在都已承诺,因此您可以直接返回承诺。这也确实有助于正确处理错误。 撤消不必要的嵌套并改为链式。 删除 Object.assign(),因为似乎没有原因。

在您的编辑中,您要求对此代码段进行解释:

return encryptPassword(args.password, (err, hash) => 
  if (err) 
    return reject(new Error('The password could not be hashed.'));
  

  return User.create(Object.assign(args,  password: hash ))
    .then((user) => 
    .catch((err2) => 

      return reject(err2);
    此代码调用encryptPassword()。 它返回可能是undefined 的结果,所以return 所做的只是控制程序流(退出包含函数),但由于在同一级别返回之后没有代码,所以这是不必要的. 您将回调传递给encryptPassword()。该回调是异步的,这意味着它会在一段时间后调用。 回调有两个参数:errhash。在 node.js 异步调用约定中,如果第一个参数是真实的(例如,包含一些真实的值),那么它代表一个错误。如果是假的(通常是null),那么没有错误,第二个参数包含异步操作的结果。 如果出现错误,则拒绝父 Promise 并退出回调。同样,reject 没有返回值,所以 return 只是用于退出回调(因此回调中没有其他代码运行)。 然后,另一个异步操作User.create() 被调用,并传递给其中设置了新密码的args 对象。 User.create() 返回一个 Promise,因此它的结果被 .then() 方法捕获并传递给它的回调。 一段时间后,当异步User.create() 完成时,它将解析其承诺,这将导致.then() 回调被调用并将结果传递给它。如果该操作出错,则不会调用.then() 回调,而是调用.catch() 回调。在那个.catch() 回调中,父承诺被拒绝。这是一个 Promise 反模式(在另一个 Promise 中解析父 Promise),因为在正确的错误处理中很容易出错。我的回答显示了如何避免这种情况。

【讨论】:

介意我扩展您的代码以提供 ES7 答案,还是您已经在着手解决这个问题? @DDave - 我添加了对这段代码的解释。你可以很好地学习 ES7,但是如果你尝试使用 async/await 却不了解 Promise 的工作原理,你会感到非常困惑并写出糟糕的代码。我已经看到很多初学者转向 ES7 并为异步操作编写看起来同步的代码,他们在错误处理和控制流方面犯了可怕的错误,因为它不是真正的同步代码。它是异步代码,你必须了解它是如何编写好的代码的,甚至是 ES7 代码。 @DDave - async/awaitpromise.then() 的语法糖。一旦您了解了 promise.then() 的工作原理、promise 链接和嵌套的工作原理以及错误如何在 promise 中传播,使用它(节省一些输入)就很方便,但它不能替代任何这些。我一直在预测一波用 ES7 语法编写的非常糟糕的异步代码,因为它可以让你非常快速地编写糟糕的代码。也可以比以前更快地编写好代码,但必须正确理解异步和错误。 @DDave - 我更改了名称,因为您的原始问题中不存在该函数的来源(或者至少我没有看到它)所以我不知道它是否可以只需编辑或者如果有其他代码以它的方式使用它。如果您拥有该功能,但一切都意味着只需编辑它以返回一个承诺。 我投票支持这个答案。这是一个很好的解释。查看 OP 的 cmets,它证明了我在回答中的观点,即在执行 async/await 之前了解回调、承诺是必不可少的。【参考方案2】:

扩展@jfriend00's answer,这是一种使用ES2017 async / await 语法来展平callback pyramid 或callback hell 的方法,但是您更喜欢这样称呼它:

const encryptPasswordPromise = require('util').promisify(encryptPassword)

const connectors = 
  Auth: 
    async signUp (args) 
      const  email, password  = args
      // Validate the data
      let err

      if (!email) 
        err =  code: 'email.empty', message: 'Email is empty.' 
       else if (!isEmail(email)) 
        err =  code: 'email.invalid', message: 'You have to provide a valid email.' 
       else if (!password) 
        err =  code: 'password.empty', message: 'You have to provide a password.' 
      

      if (err) 
        throw err
      

      let hash

      try 
        hash = await encryptPasswordPromise(password)
       catch (err) 
        throw new Error('The password could not be hashed.')
      

      const  _id: id, email  = await User.create(Object.assign(args,  password: hash ))

      try 
        return createToken( id, email )
       catch (err) 
        if (err.code === 11000) 
          throw  code: 'user.exists', message: 'There is already a user with this email.' 
         else 
          throw err
        
      
    
  


module.exports = connectors

我选择使用 node.js 内置转换函数,而不是手写我自己的基于承诺的 encryptPassword()

总体而言,仍然可以进行一些改进,例如将 signUp() 参数的验证移至单独的函数,但这些都与扁平化“回调地狱”反模式有关基于 promise 的控制流和 async/await 语法。

【讨论】:

你不相信分号? @jfriend00 我最近(今年年初)采用了这种风格,我觉得这很合适:standardjs.com。我意识到这不是官方认可的标准或类似的东西,这只是我现在更喜欢的写作方式。 对于那些习惯于在语句末尾使用分号进行模式识别的人来说,这真的会让你大吃一惊。使经验丰富的老手更难立即阅读代码。我把它们放进去是因为当它们丢失时我被奇怪的边缘情况咬伤了,而且它可能会产生非常令人费解的错误(错误报告与实际错误相差很多行)。我宁愿让最小化程序为浏览器代码删除它们,并将它们留在 node.js 代码中。但是,对于每个人来说,我猜是。 就我个人而言,我从来没有在错误的行上出现错误,而且由于 standard 带有自己的 linter,我的 npm publish 从字面上阻止我提交由于自动分号而可能存在错误的代码插入,所以这对我来说从来都不是问题。 async/await 是 ES2017 的一部分,而不是 ES7 (ES2016)。【参考方案3】:

首先要做的事情。您的代码中没有循环。

如果您来自 PHP,那么我猜 Javascript 的异步执行模型对您来说似乎很陌生。我建议学习更多关于 Javascript 的知识。

按以下顺序学习以下内容:

    回调 承诺 发电机 异步/等待

这些是处理 Javascript 异步执行模型的不同方法,从最基本的(回调)到最现代的方法“async/await”。

Async/await 将是最易读的,但如果您了解 async/await 是如何产生的会更好,因为如果您不了解自己在做什么,它们很容易以错误的方式使用。

【讨论】:

Generators before 承诺?有趣的建议。 虽然我同意生成器是 JavaScript 中的一个重要的高级主题,但我并不真正了解它们如何解决这里的问题。你能详细说明你为什么提到这些吗? 与其说是要学习的回调,不如说是异步操作和异步回调。与array.map() 一起使用的回调没什么大不了的,而且很容易理解。而且,我同意大多数人都想在生成器之前学习和使用 Promise。另外,我真的不确定这一切与 OP 所要求的完全相关。 请阅读我的问题中的更新,代码不是我的,我只是尝试理解代码并寻找更新到 es7,回调和承诺让我抓狂 @DDave: async 函数返回承诺。 async/await 是 promise 的语法糖,所以你至少应该理解这些。

以上是关于避免在 javascript 中循环多次返回 - async / await 以解决回调金字塔或回调地狱,的主要内容,如果未能解决你的问题,请参考以下文章

Javascript处理对象数组

Javascript处理对象数组

在 Javascript 中,当执行深度复制时,由于属性是“this”,我如何避免循环?

JavaScript的基本规范

Javascript Promise 多次返回.

避免多次加入同一张表