“流程控制”本来是件比较简单的事,但是由于Nodejs的异步架构的实现方法,对于需要同步的业务逻辑,实现起来就比较麻烦。嵌套3-4层,代码就会变得的支离破碎了!今天就遇到了一个业务逻辑,连续对数据库操作,前后有依赖。让我们看看Async是如何解决问题的。
1 Async介绍
Async是一个流程控制工具包,提供了直接而强大的异步功能
。基于Javascript为Node.js设计,同时也可以直接在浏览器中使用。
Async提供了大约20个函数,包括常用的 map, reduce, filter, forEach 等,异步流程控制模式包括,串行(series),并行(parallel),瀑布(waterfall)等
。
项目地址:https://github.com/caolan/async
2 Async安装
安装async有两个方式:
独立安装async
下载async demo代码安装
2.1 独立安装async
~ D:\workspace\javascript>mkdir nodejs-async && cd nodejs-async
~ D:\workspace\javascript\nodejs-async>npm install async
npm http GET https://registry.npmjs.org/async
npm http 304 https://registry.npmjs.org/async
async@0.2.9 node_modules\async
打开网页,参照示例学习:https://github.com/bsspirit/async_demo
2.2 下载async demo代码安装
~ D:\workspace\javascript>git clone git@github.com:bsspirit/async_demo.git nodejs-async
~ D:\workspace\javascript>cd nodejs-async
~ D:\workspace\javascript\nodejs-async>npm install
npm http GET https://registry.npmjs.org/moment
npm http GET https://registry.npmjs.org/async
npm http 304 https://registry.npmjs.org/moment
npm http 304 https://registry.npmjs.org/async
async@0.2.9 node_modules\async
moment@2.1.0 node_modules\moment
`
这套demo示例,比较全面的介绍了async的使用,有中文注释。
3 Async函数介绍
基于async的0.2.9版本。async主要实现了三个部分的流程控制功能:
集合: Collections
流程控制: Control Flow
工具类: Utils
3.1 集合: Collections
each: 如果想对
同一个集合中的所有元素都执行同一个异步
操作。map:
对集合中的每一个元素,执行某个异步操作,得到结果
。所有的结果将汇总到最终的callback里。与each的区别是,each只关心操作不管最后的值,而map关心的最后产生的值。filter:
使用异步操作对集合中的元素进行筛选
, 需要注意的是,iterator的callback只有一个参数,只能接收true或false。reject: reject跟filter正好相反,当测试为true时则抛弃
reduce: 可以让我们给定一个初始值,用它与集合中的每一个元素做运算,最后得到一个值。reduce从左向右来遍历元素,如果想从右向左,可使用reduceRight。
detect: 用于取得集合中满足条件的第一个元素。
sortBy: 对集合内的元素进行排序,依据每个元素进行某异步操作后产生的值,从小到大排序。
some: 当集合中是否有至少一个元素满足条件时,最终callback得到的值为true,否则为false.
every: 如果集合里每一个元素都满足条件,则传给最终回调的result为true,否则为false
concat: 将多个异步操作的结果合并为一个数组。
3.2 流程控制: Control Flow
series: 串行执行,一个函数数组中的每个函数,每一个函数执行完成之后才能执行下一个函数。
parallel: 并行执行多个函数,每个函数都是立即执行,不需要等待其它函数先执行。传给最终callback的数组中的数据按照tasks中声明的顺序,而不是执行完成的顺序。
whilst: 相当于while,但其中的异步调用将在完成后才会进行下一次循环。
doWhilst: 相当于do…while, doWhilst交换了fn,test的参数位置,先执行一次循环,再做test判断。
until: until与whilst正好相反,当test为false时循环,与true时跳出。其它特性一致。
doUntil: doUntil与doWhilst正好相反,当test为false时循环,与true时跳出。其它特性一致。
forever: 无论条件循环执行,如果不出错,callback永远不被执行。
waterfall: 按顺序依次执行一组函数。每个函数产生的值,都将传给下一个。
compose: 创建一个包括一组异步函数的函数集合,每个函数会消费上一次函数的返回值。把f(),g(),h()异步函数,组合成f(g(h()))的形式,通过callback得到返回值。
applyEach: 实现给一数组中每个函数传相同参数,通过callback返回。如果只传第一个参数,将返回一个函数对象,我可以传参调用。
queue: 是一个串行的消息队列,通过限制了worker数量,不再一次性全部执行。当worker数量不够用时,新加入的任务将会排队等候,直到有新的worker可用。
cargo: 一个串行的消息队列,类似于queue,通过限制了worker数量,不再一次性全部执行。不同之处在于,cargo每次会加载满额的任务做为任务单元,只有任务单元中全部执行完成后,才会加载新的任务单元。
auto: 用来处理有依赖关系的多个任务的执行。
iterator: 将一组函数包装成为一个iterator,初次调用此iterator时,会执行定义中的第一个函数并返回第二个函数以供调用。
apply: 可以让我们给一个函数预绑定多个参数并生成一个可直接调用的新函数,简化代码。
nextTick: 与nodejs的nextTick一样,再最后调用函数。
times: 异步运行,times可以指定调用几次,并把结果合并到数组中返回。
timesSeries: 与time类似,唯一不同的是同步执行。
3.3 工具类: Utils
memoize: 让某一个函数在内存中缓存它的计算结果。对于相同的参数,只计算一次,下次就直接拿到之前算好的结果。
unmemoize: 让已经被缓存的函数,返回不缓存的函数引用。
log: 执行某异步函数,并记录它的返回值,日志输出。
dir: 与log类似,不同之处在于,会调用浏览器的console.dir()函数,显示为DOM视图。
noConflict: 如果之前已经在全局域中定义了async变量,当导入本async.js时,会先把之前的async变量保存起来,然后覆盖它。仅仅用于浏览器端,在nodejs中没用,这里无法演示。
async_demo使用介绍,详细使用请参考github源代码:https://github.com/bsspirit/async_demo每个函数的用法,有非常详细的实例!!
4 场景:对数据库的连续操作
原场景中,对数据串行操作,增删改查(CRUD),代码如下:
var mysql = require('mysql');
var conn = mysql.createConnection({
host: 'localhost',
user: 'nodejs',
password: 'nodejs',
database: 'nodejs',
port: 3306
});
conn.connect();
var insertSQL = 'insert into t_user(name) values("conan"),("fens.me")';
var selectSQL = 'select * from t_user limit 10';
var deleteSQL = 'delete from t_user';
var updateSQL = 'update t_user set name="conan update" where name="conan"';
//delete
conn.query(deleteSQL, function (err0, res0) {
if (err0) console.log(err0);
console.log("DELETE Return ==> ");
console.log(res0);
//insert
conn.query(insertSQL, function (err1, res1) {
if (err1) console.log(err1);
console.log("INSERT Return ==> ");
console.log(res1);
//query
conn.query(selectSQL, function (err2, rows) {
if (err2) console.log(err2);
console.log("SELECT ==> ");
for (var i in rows) {
console.log(rows[i]);
}
//update
conn.query(updateSQL, function (err3, res3) {
if (err3) console.log(err3);
console.log("UPDATE Return ==> ");
console.log(res3);
//query
conn.query(selectSQL, function (err4, rows2) {
if (err4) console.log(err4);
console.log("SELECT ==> ");
for (var i in rows2) {
console.log(rows2[i]);
}
});
});
});
});
});
//conn.end();
为了实现了串行操作,所有的调用都是在callback中实现的,5层嵌套结构。这种代码已经变得不可以维护了。所以,需要用async库,对上面的代码结构进行重写!
var mysql = require('mysql');
var async = require('async');
var conn = mysql.createConnection({
host: 'localhost',
user: 'nodejs',
password: 'nodejs',
database: 'nodejs',
port: 3306
});
var sqls = {
'insertSQL': 'insert into t_user(name) values("conan"),("fens.me")',
'selectSQL': 'select * from t_user limit 10',
'deleteSQL': 'delete from t_user',
'updateSQL': 'update t_user set name="conan update" where name="conan"'
};
var tasks = ['deleteSQL', 'insertSQL', 'selectSQL', 'updateSQL', 'selectSQL'];
async.eachSeries(tasks, function (item, callback) {
console.log(item + " ==> " + sqls[item]);
conn.query(sqls[item], function (err, res) {
console.log(res);
callback(err, res);
});
}, function (err) {
console.log("err: " + err);
});
控制台输出:
deleteSQL ==> delete from t_user
{ fieldCount: 0,
affectedRows: 0,
insertId: 0,
serverStatus: 34,
warningCount: 0,
message: '',
protocol41: true,
changedRows: 0 }
insertSQL ==> insert into t_user(name) values("conan"),("fens.me")
{ fieldCount: 0,
affectedRows: 2,
insertId: 45,
serverStatus: 2,
warningCount: 0,
message: '&Records: 2 Duplicates: 0 Warnings: 0',
protocol41: true,
changedRows: 0 }
selectSQL ==> select * from t_user limit 10
[ { id: 45,
name: 'conan',
create_date: Fri Sep 13 2013 12:24:51 GMT+0800 (中国标准时间) },
{ id: 46,
name: 'fens.me',
create_date: Fri Sep 13 2013 12:24:51 GMT+0800 (中国标准时间) } ]
updateSQL ==> update t_user set name="conan update" where name="conan"
{ fieldCount: 0,
affectedRows: 1,
insertId: 0,
serverStatus: 2,
warningCount: 0,
message: '(Rows matched: 1 Changed: 1 Warnings: 0',
protocol41: true,
changedRows: 1 }
selectSQL ==> select * from t_user limit 10
[ { id: 45,
name: 'conan update',
create_date: Fri Sep 13 2013 12:24:51 GMT+0800 (中国标准时间) },
{ id: 46,
name: 'fens.me',
create_date: Fri Sep 13 2013 12:24:51 GMT+0800 (中国标准时间) } ]
err: null
代码一下读性就增强了许多倍,这就是高效的开发。
5 Async多任务时间管理
做服务器端开发时,经常会遇到时间管理的功能需求,比如每2秒刷新一次,每三分钟做一次统计计算,周一至周五9点30启动一个定时任务等等。很多时候我们会把这些定时任务,交给linux系统的Crontab来实现。不过,有时为了增加系统的灵活性,我们需要在服务器后台实现。
对于单线程的Nodejs,如何控制多任务的时间管理呢?
5.1 需求描述
基于Nodejs的express3构建的web框架,需要在周一至周五,早上9点15分时,分别启动程序A和程序B,程序C。下午16点程序A,B,C停止。
程序A: 每1秒去redis取数据一次,保留在Nodejs的全局变量G中。
程序B: 每10秒去mysql取数据一次,通过websocket直接访问给客户端。
程序C: 每5秒对全局变量G,进行平均值计算,然后通过websocket直接访问给客户端。
5.2 Nodejs的实现方案setInterval
初始化项目:
~ cd D:\workspace\javascript\nodejs-async\demo
~ express -e timers
~ cd timers && npm install
~ npm install moment
~ npm install twix
编辑文件:app.js,在文件最面下增加新代码
...
//moment,twix时间工具
var moment = require('moment')
,twix = require('twix');
//判断程序启动时间
function isTime(){
var hms = 'HHmmss';
return moment("091500",hms).twix(moment("160000",hms)).contains(moment());
}
//打印日志
if(isTime()){
console.log("===============Working time===================");
}
//日志时间格式化
function now() {
return moment().format("HH:mm:ss");
}
//全局变量G
var G = 0;
//模拟程序A
function A() {
console.log(now() + " A(s1)=> {G:" + (G++) + "} Cache G");
}
//模拟程序B
function B() {
console.log(now() + " B(s10)=> {B:10} TO client");
}
//模拟程序C
function C() {
console.log(now() + " C(s5)=> {G:" + (G / 5) + "} TO client");
G = 0;
}
//分别对A,B,C程序进行时间管理
setInterval(function () {
if(isTime()){
A()
};
}, 1000);
setInterval(function () {
if(isTime()){
C();
}
}, 5 * 1000);
setInterval(function () {
if(isTime()) {
B();
}
}, 10 * 1000);
运行nodejs,查看日志输出:
D:\workspace\javascript\nodejs-async\demo\timers>node app.js
===============Working time===================
Express server listening on port 3000
15:02:24 A(s1)=> {G:0} Cache G
15:02:25 A(s1)=> {G:1} Cache G
15:02:26 A(s1)=> {G:2} Cache G
15:02:27 A(s1)=> {G:3} Cache G
15:02:28 A(s1)=> {G:4} Cache G
15:02:28 C(s5)=> {G:1} TO client
15:02:29 A(s1)=> {G:0} Cache G
15:02:30 A(s1)=> {G:1} Cache G
15:02:31 A(s1)=> {G:2} Cache G
15:02:32 A(s1)=> {G:3} Cache G
15:02:33 A(s1)=> {G:4} Cache G
15:02:33 C(s5)=> {G:1} TO client
15:02:33 B(s10)=> {B:10} TO client
15:02:34 A(s1)=> {G:0} Cache G
15:02:35 A(s1)=> {G:1} Cache G
15:02:36 A(s1)=> {G:2} Cache G
15:02:37 A(s1)=> {G:3} Cache G
15:02:38 A(s1)=> {G:4} Cache G
15:02:38 C(s5)=> {G:1} TO client
15:02:39 A(s1)=> {G:0} Cache G
15:02:40 A(s1)=> {G:1} Cache G
15:02:41 A(s1)=> {G:2} Cache G
15:02:42 A(s1)=> {G:3} Cache G
程序A,每1秒运行一次,给G+1。
程序B,每10秒运行一次,输出到客户端。
程序C,每5秒运行一次,取G的平均值,给G赋值为0,输出到客户端。
虽然完成了功能需求,但是代码不美观!如果再增加任务D,E,F….代码不好维护。
5.3 Async多任务时间管理
下面我们用async包,对上面的浏览进行封装。
//moment,twix时间工具
var moment = require('moment')
,twix = require('twix');
//判断程序启动时间
function isTime(){
var hms = 'HHmmss';
return moment("091500",hms).twix(moment("160000",hms)).contains(moment());
}
//打印日志
if(isTime()){
console.log("===============Working time===================");
}
//日志时间格式化
function now() {
return moment().format("HH:mm:ss");
}
//全局变量G
var G = 0;
//模拟程序A
function A() {
console.log(now() + " A(s1)=> {G:" + (G++) + "} Cache G");
}
//模拟程序B
function B() {
console.log(now() + " B(s10)=> {B:10} TO client");
}
//模拟程序C
function C() {
console.log(now() + " C(s5)=> {G:" + (G / 5) + "} TO client");
G = 0;
}
var async = require('async');
var arr = [
{fun: A, delay: 1000, test: isTime},
{fun: B, delay: 10 * 1000, test: isTime},
{fun: C, delay: 5 * 1000, test: isTime}
];
async.each(arr, function (item, callback) {
async.whilst(item.test,function(cb) {
item.fun();
setTimeout(cb, item.delay);
},function(err) {
console.log("Not working time!");
}
);
}, function (err) {
log('Error: ' + err);
});
构建一个arr数组,封装调用的A,B,C的参数。
使用async.each函数,对arr的item异步并行
使用async.whilst函数,对任务启动时间进行判断,并根据delay运行任务
程序运行结果与setInterval一样。而代码更利于维护,一旦需要增减任务,简单地修改arr的数组就行了
,其他的代码都不用动!!