异步nodejs代码的同步样子写法样例
js的异步嵌套太深代码将不好看。尤其在用node的时候这种情况会大量出现。
这里用node连接redis,做一个用户注册的简单例子来说明。例如用redis做存储。我们设置一个String类型的key, seq:user这个作为自增主键ID,set类型allUser存放所有用户ID,和set类型allUserName存放所有用户名。
代码示例如下
var redis = require("redis"),
db = redis.createClient(6379, "127.0.0.1");
db.on("connect", function() {
main();
});
function main() {
insertTest();
}
var insertTest = function() {
db.incr("seq:user", function(err, userID) {
if (err) {
console.log(err);
} else {
var username = "username001";
db.set("user:" + userID + ":username", username, redis.print);
db.set("user:" + userID + ":passwd", "111111", redis.print);
db.set("username:" + username + ":uid", userID, redis.print);
db.sadd("allUserName", username, redis.print);
db.sadd("allUser", userID, function() {
console.log("OK");
});
}
});
};
逻辑很简单,先通过incr seq:user拿到自增的ID成功之后,下一步一个个保持字段信息和列表数据。
上面代码也可以改写成这个样子
var getLastUserID = function(callback) {
db.incr("seq:user", function(err, userID) {
if (!err) {
callback(userID);
}
});
};
var insertUser = function(userID) {
var username = "username001";
db.set("user:" + userID + ":username", username, redis.print);
db.set("user:" + userID + ":passwd", "111111", redis.print);
db.set("username:" + username + ":uid", userID, redis.print);
db.sadd("allUserName", username, redis.print);
db.sadd("allUser", userID, function() {
console.log("OK");
});
};
var insertTest = function() {
getLastUserID(insertUser);
};
首先要保证的是insertUser函数里面所有的插入语句顺序OK。因为我们只在最后一此插入的回调中做业务处理。中间插入语句有没有成功程序无法知晓。还有连续的同服务器交互增加了不少网络IO量。
这里的改进,使用redis的pipeline。
var insertUser = function(userID) {
var username = "username001";
var multi = db.multi();
multi.set("user:" + userID + ":username", username);
multi.set("user:" + userID + ":passwd", "111111");
multi.set("username:" + username + ":uid", userID);
multi.sadd("allUser", userID);
multi.sadd("allUserName", username);
multi.exec(function(err, replies) {
if (!err) {
console.log("OK");
}
});
};
var isUserNameExists = function(username, callback1, callback2) {
db.sismember("allUserName", username, function(err, isMember) {
if (!err && !isMember) {
callback1(username, callback2);
}
});
};
var getLastUserID = function(username, callback) {
db.incr("seq:user", function(err, userID) {
if (!err) {
callback(username, userID);
}
});
};
var insertUser = function(username, userID) {
var multi = db.multi();
multi.set("user:" + userID + ":username", username);
multi.set("user:" + userID + ":passwd", "111111");
multi.set("username:" + username + ":uid", userID);
multi.sadd("allUser", userID);
multi.sadd("allUserName", username);
multi.exec(function(err, replies) {
if (!err) {
console.log("OK");
}
});
};
var insertTest = function() {
var username = "username003";
isUserNameExists(username, getLastUserID, insertUser);
};
或者是用匿名函数的嵌套,写在一个地方
db.sismember("allUserName", username, function(err, isMember) {
if (!err) {
if (!isMember) {
db.incr("seq:user", function(err, userID) {
if (!err) {
var multi = db.multi();
multi.set("user:" + userID + ":username", username);
multi.set("user:" + userID + ":passwd", passwd);
multi.set("username:" + username + ":uid", userID);
multi.sadd("allUser", userID);
multi.sadd("allUserName", username);
multi.exec(function(err, replies) {
if (!err) {
console.log("OK");
}
});
}
});
}
}
});
两个方式阅读起来都不方便。更何况我们的示例代码逻辑并不是十分复杂,还没有加异常处理了。即便这样也是很难阅读的。
js的promise标准可以做到类似同步代码一些的写法。还有一些其他的js库也提供一些相应的同步写法。
这里介绍个叫async的库。地址在https://github.com/caolan/async
下载安装好后引入
var async = require("async");
利用async的waterfall可以指定一组函数的执行顺序,并且可以根据中间状态做流程控制。
上面的代码就可以改写成如下样子
var insertTest = function() {
var username = "username005";
async.waterfall([
function(callback) {
db.sismember("allUserName", username, callback);
},
function(isMember, callback) {
if (!isMember) {
db.incr("seq:user", callback);
} else {
callback(new Error("username has exist"), null);
}
},
function(userID, callback) {
var multi = db.multi();
multi.set("user:" + userID + ":username", username);
multi.set("user:" + userID + ":passwd", "111111");
multi.set("username:" + username + ":uid", userID);
multi.sadd("allUser", userID);
multi.sadd("allUserName", username);
multi.exec(function(err, replies) {
if (!err) {
callback(null, "OK");
} else {
callback(new Error("save data fail"), null);
}
});
}
], function (err, result) {
console.log(err);
console.log(result);
});
};
它会按照定义好的函数列表挨个执行,并且上一个函数执行的结果很方便地传入到下一个函数中做判断。上例代码中因为js约定的机制,回调第一个参数err,第二个是正确结果。所以传递的过程都不需要自己写。
这样改写之后不光看起来清楚,流程控制起来也更加方便。例如如果用户名重复的错误,或者是保存数据失败的错误发生了,或者是全部调用成功了。都可以在最后的统一结果处理函数中处理掉。
使用async.waterfall做流程控制个人感觉比用promise库要简单方便。