Null's Blog

Nodejs中的async组件

U know that? callbackhell!

nodejscallbackhell

言归正传

nodejs开发无法避免的无尽的callback,有人觉得奇丑无比,有人觉得无比优雅(比如我)。
本博客源码首页为例,涉及数据查询的地方包括:页面主要区域的数据(list,article等),右侧标签,右侧归档等。在设计之初,这些全部都来自于mongodb的posts集合,但每一个页面,都需要查询至少3次数据库,如果包含分页,则是4次。这里暂不评价这个设计思路是否合理,仅以此举例。
可想而知,如果每个函数均以nodejs推荐的callback形式设计,就算是碰上了所谓的“callbackhell”。
这里引用一段网络的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var class=require('./module/class');
export.pageStudent=function(req,res){
var rtnJson={};
class.getStudent('1班',function(error,oneResult){
if(!error&&oneResult){
rtnJson['1班']=oneResult;
class.getStudent('2班',function(error,twoResult){
if(!error&&twoResult){
rtnJson['2班']=twoResult;
class.getStudent('3班',function(error,threeResult){
if(!error&&threeResult){
rtnJson['3班']=threeResult;
//3个班级全部获取完成
res.render('./veiw/pageStudent',{students:rtnJson});
}else{
res.render('./veiw/pageStudent',{students:rtnJson});
}
});
}else{
res.render('./veiw/pageStudent',{students:rtnJson});
}
});
}else{
res.render('./veiw/pageStudent',{students:rtnJson});
}
});
}

因此,async横空出世,这里以本博客源码为例,解释async常用的3种使用场景

串行且有关联(waterfall):一个function完成后将结果以参数形式传递跟下一个function,最后是async的callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//routes/post.js
async.waterfall([
function(done){
var post = new Post({});
post.getAllTag(function(err,docs){
var tags=[];
if(!(err)&&docs){
//判断是否勾选了任何一个 tag
if(formData.hasOwnProperty('tags')){
var tagsArrayStr = formData.tags.toString();
var tagsArray = tagsArrayStr.split(',');
//1.保存勾选的tag
for(var i=0;i<tagsArray.length;i++){
if(tagsArray[i]==='newtags'){
continue;
}else{
for(var j=0;j<docs.length;j++){
if(tagsArray[i]===docs[j].tag){
tags.push({tag:tagsArray[i],tagName:docs[j].tagName});
break;
}
}
}
}
//2.保存新增的 tag
var newtagsStr = formData.newtags;
if(tagsArrayStr.indexOf('newtags')>-1&&newtagsStr.length>0){
var newTagArray = newtagsStr.split(',');
for(var i=0;i<newTagArray.length;i++){
var newTagTemp = newTagArray[i].split(':')[0];
var newTagNameTemp = newTagArray[i].split(':')[1];
//防止重复添加相同标签
var isHas = false;
for(var j=0;j<docs.length&&isHas==false;j++){
if(newTagTemp===docs[j].tag){
isHas=true;
break;
}
}
if(!isHas){
tags.push({tag:newTagTemp,tagName:newTagNameTemp});
}
}
}
}
done(null,tags);
}
});
},
function(tags,done){
var date = new Date(formData.date);
var post = new Post({
name:formData.name,
title:formData.title,
date:date,
tags:tags,
post:formData.post
});
post.save(function(err){
if(err===null){
done(null);
}
});
}
],function(asyncErr,asyncResult){
res.redirect('/post');
});

并行(parallel):每个function执行完后,传递结果给async的callback,统一使用所有结果或错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
async.parallel({
getAllTag:function(done){
post.getAllTag(function(err,docs){
if(!(err)&&docs){
done(null,docs);
}else{
done(null);
}
});
},
getArchive:function(done){
list.getArchive(function(err,archiveArray){
if(!(err)&&archiveArray){
done(null,archiveArray);
}else{
done(null);
}
});
},
getPageCount:function(done){
list.getCount(function(err,count){
if(!(err)&&(count!=0)){
done(null,Math.ceil(count/settings.pageSize));
}else{
done(null);
}
});
},
getList:function(done){
list.getList(function(err,docs){
if(!(err)&&docs){
done(null,docs);
}else{
done(null);
}
});
}
},function(asyncErr,asyncResult){
if(!asyncErr){
res.render('list',{
list:asyncResult.getList,
archiveList:asyncResult.getArchive,
tags:asyncResult.getAllTag,
pagination:{
pageIndex:1,
pageCount:asyncResult.getPageCount
}
});
}else{
//404
res.end();
}
});

另外还有一种相当特殊的情况

auto:可组合串行任务与并行任务,修改博文的时候使用了这种

业务逻辑为修改博文时,需要读取所有的tags,并把原博文选中的tags勾上,当然,碰见这种情况可以放弃使用并行(parallel)全部使用串行(waterfall),但这样就无法相对完整体现async组件的优势了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
async.auto({
getAllTag:function(done){
post.getAllTag(function(err,docs){
if(!(err)&&docs){
done(null,docs);
}
});
},
getArchive:function(done){
list.getArchive(function(err,archiveArray){
done(null,archiveArray);
});
},
getArticle:function(done){
article.getEdit(function(err,doc){
if(!(err)&&doc){
done(null,doc);
}else{
//done(404);
done(null,doc);
}
});
},
//等待getAllTag与getArticle完成,并组合它们
setChooseTags:['getAllTag','getArticle',function(done,results){
var allTags = results.getAllTag;
var articleTags = results.getArticle.tags;
for(var i=0;i<allTags.length;i++){
for(var j=0;j<articleTags.length;j++){
if(allTags[i].tag===articleTags[j].tag){
allTags[i].checked = "checked";
break;
}
}
}
done(null,allTags);
}]
},function(asyncErr,asyncResult){
if(!asyncErr){
res.render('post',{
article:asyncResult.getArticle,
archiveList:asyncResult.getArchive,
tags:asyncResult.setChooseTags
});
}else{
//404
res.send('404');
res.end();
}
});