JS里如何将多个异步调用转同步调用
本文介绍如何在js里将多个异步调用优雅地转同步调用,可适用于vue, node.js等编程
背景说明
最近在处理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");
}
}
小结
- 在js里很多引用都是callback异步执行,除mongoDB调用外,如axios,request调用都是,所以需要转同步。很多时候,如果代码不按预期执行,就看看是不是异步执行了。
- Promise的reslove和reject只能传一个值,所以如传多个参数,可通过{}封装成一个对象。
- 通过方式1和2整理,不光解决了问题,同时也优化了代码,代码看上去也更清晰了。
更多推荐
所有评论(0)