背景说明

最近在处理node.js和vue问题过程开发中,经常遇到需要将多个异步函数转同步调用的场景。如果不转同步调用,代码逻辑会不按预期走。如果在异步函数里调用异步函数,这种嵌套调用,当超过2层时,显得代码很凌乱。
首先列一下最早有问题的代码片段。原始作者以为会顺序执行,其实这些调用都是异步执行,也就是我们常说的callback回调模式。

      // 这里的db是mongoDB, db.users对应mongo里的users表
      
      db.users.findOne({ user_email: req.body.email }, (err, user) => {
        if(user){
          // user already exists with that email address
          console.log("user:'" + user.user_email + "' is already exists!");
        }else{
          // email is ok to be used.
          db.users.insert(doc, (err, doc) => {
            if(err){
              console.error("Failed to insert user: " + err);
              req.session.message = req.i18n.__("Failed to insert user to DB");
              req.session.message_type = "danger";
              res.redirect(req.app_context + "/login");
            }
          });
        }
      });

      db.users.findOne({ user_email: req.body.email }, (err, user) => {
        // check if user exists with that email
        if(user === undefined || user === null){
          req.session.message = req.i18n.__(
            "A user with that email does not exist."
          );
          req.session.message_type = "danger";
          res.redirect(req.app_context + "/login");
        }else{
            req.session.user = req.body.email;
            req.session.users_name = user.users_name;
            req.session.user_id = user._id.toString();
            req.session.is_admin = user.is_admin;
            res.redirect(req.app_context + "/");
        }
      });

上面代码逻辑本意是:先从库里查找记录,如不存在就添加。然后再查找,如果找到,就转"/", 否则转"/login"。 因为findOne和insert都是callback调用,几乎是同时执行,压根没按预期执行。如果按嵌套调用就如下,代码不优雅,if-else跨多行,冗余代码很多。

// 嵌套调用
      db.users.findOne({ user_email: req.body.email }, (err, user) => {
        if(user){
          // user already exists with that email address
          console.log("user:'" + user.user_email + "' is already exists!");
            req.session.user = req.body.email;
            req.session.users_name = user.users_name;
            req.session.user_id = user._id.toString();
            req.session.is_admin = user.is_admin;
            res.redirect(req.app_context + "/");
            return;
        }else{
          // email is ok to be used.
          db.users.insert(doc, (err, doc) => {
            if(err){
              console.error("Failed to insert user: " + err);
              req.session.message = req.i18n.__("Failed to insert user to DB");
              req.session.message_type = "danger";
              res.redirect(req.app_context + "/login");
              return;
            }

            db.users.findOne({ user_email: req.body.email }, (err, user) => {
            // check if user exists with that email
                if(user === undefined || user === null){
                    req.session.message = req.i18n.__(
                    "A user with that email does not exist."
                    );
                    req.session.message_type = "danger";
                    res.redirect(req.app_context + "/login");
                }else{
                    req.session.user = req.body.email;
                    req.session.users_name = user.users_name;
                    req.session.user_id = user._id.toString();
                    req.session.is_admin = user.is_admin;
                    res.redirect(req.app_context + "/");
                }
            });
            
          });
        }
      });

如何解决上述问题,我找到两种方式,列在下面。两个都是利用Promise对象:执行正常,通过reslove()往下传递;执行有异常或希望退出,就通过reject()来实现。

函数定义

在将原来异步逻辑变同步执行前,先对提炼出代码的重用部分,做两个函数, 定义在utils.js里。一个是查找findUser,另一个是添加insertUser。它们都通过Promise返回。
【备注】这里的代码是在node.js运行的,如是vue的单页面模式,则改下export方式

function findUser(info){
    return new Promise(function (resolve, reject) {
      const req = info.req;
      const db = info.req.app.db;
      db.users.findOne({ user_email: info.doc.user_email }, (err, user) => {
        if ( err !== null) {
          return reject("no_user");
        }
        return resolve(
          {
            req: req,
            doc: info.doc,
            user: user
          }
        );
      });
    });
}
  
function insertUser(info){
    const req = info.req;
    const user = info.user;
    const doc = info.doc;
    return new Promise(function (resolve, reject) {
      if(user !== null && user !== undefined){
        return resolve(user);
      }
  
      const db = req.app.db;
  
      db.users.insert(doc, (err, info) => {
        if(err){
          return reject("failed_to_insert_user");
        } 
        //return resolve(info);
  
        db.users.findOne({ user_email: info.doc.user_email }, (err, user) => {
            return resolve(user);
        });
      });
    });
}

module.exports = {
    findUser,
    insertUser
}

方式1 (Promise方式)

先new一个Promise对象,然后通过then顺序调用,catch捕获异常。这里的err要用=>箭头函数。和方式2的err要区别下。

    const utils = require("./utils");

    //.......省略很多代码
    
    var pm = Promise.resolve({req:req,doc:doc}); //promise模式

    pm.then(utils.findUser)
      .then(utils.insertUser)
      .then(info =>{
        req.session.user = info.user_email;
        req.session.users_name = info.users_name;
        req.session.user_id = info._id.toString();
        req.session.is_admin = info.is_admin;
        req.session.fromCloud = false;
        res.redirect(req.app_context + "/");
      })
      .catch(err => {
        if( err === 'no_user') {
          req.session.message = req.i18n.__("A user with that email does not exist.")
        }else if ( err === 'failed_to_insert_user'){
          req.session.message = req.i18n.__("Failed to insert user to DB");
        }else {
          req.session.message = JSON.stringify(err);
        }
        req.session.message_type = "danger";
        res.redirect(req.app_context + "/login");
      });

方式2 (async+await方式)

首先需定义async函数doLogin, 然后在里面通过await调用utils里的findUser和insertUser函数。通过try-catch捕获异常。

   const utils = require("./utils");

    // sets up the document
    const doc = {
      users_name: body.name,
      user_email: req.body.email,
      user_password: bcrypt.hashSync(body.userId, saltRounds),
      is_admin: is_admin
    };

    doLogin(req, res, doc); // await模式

  //.......省略很多代码
  
  async function doLogin(req,res,doc){
  try{
    var info = await utils.findUser({req: req, doc:doc});
    info = await utils.insertUser(info);
    req.session.user = info.user_email;
    req.session.users_name = info.users_name;
    req.session.user_id = info._id.toString();
    req.session.is_admin = info.is_admin;
    req.session.fromCloud = false;
    res.redirect(req.app_context + "/");
  }catch(err)  {
    if( err === 'no_user') {
      req.session.message = req.i18n.__("A user with that email does not exist.")
    }else if ( err === 'failed_to_insert_user'){
      req.session.message = req.i18n.__("Failed to insert user to DB");
    }else {
      req.session.message = JSON.stringify(err);
    }
    req.session.message_type = "danger";
    res.redirect(req.app_context + "/login");
  }

}

小结

  1. 在js里很多引用都是callback异步执行,除mongoDB调用外,如axios,request调用都是,所以需要转同步。很多时候,如果代码不按预期执行,就看看是不是异步执行了。
  2. Promise的reslove和reject只能传一个值,所以如传多个参数,可通过{}封装成一个对象。
  3. 通过方式1和2整理,不光解决了问题,同时也优化了代码,代码看上去也更清晰了。
Logo

前往低代码交流专区

更多推荐