2个场景, 回调不再是黑洞!

在我看来,异步的控制主要分为两类:

无依赖的异步执行,各个操作之间没有相互依赖,均为独立,需要所有操作都执行结束后返回值.

例如场景1: 有3份txt数据文件, 要求读取这3份数据文件, 并按读取顺序依次输出文件内容.

若直接回调, 不采用任何异步控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//已经嵌套了3次了哟, 黑洞已经出现了!
fs.readFile('1.txt', 'utf8', function (err, data) {
if (err) throw err;
console.log(data);
fs.readFile('2.txt', 'utf8', function (err, data) {
if (err) throw err;
console.log(data);
fs.readFile('3.txt', 'utf8', function (err, data) {
if (err) throw err;
console.log(data);
console.timeEnd("test");
});
});
});

有相互依赖的异步执行,下一个操作需要用到上一个操作的执行结果,相当于需要顺序执行,不能并发.

例如场景2: 让我们来模拟一个真实的mongodb场景, 相对于我这样的初学者而言, 在编写应用时, 数据库CRUD中最易出现回调嵌套, 因为我们往往要利用上一步查询的结果来继续下一步.

现在我们有一份学生考试信息的文档, 记录学生的考试号、姓名、密码、分数等信息, 具体如下

{ examid: 1, username: ‘Tom’, password: md5(‘good’), score: [84, 79, 91, 90] };
{ examid: 1, username: ‘Ross’, password: md5(‘both’), score: [81, 73, 67, 78] };
{ examid: 2, username: ‘Alisa’, password: md5(‘great’), score: [78, 69, 87, 71] };
{ examid: 2, username: ‘Joan’, password: md5(‘root’), score: [98, 76, 89, 90] };
{ examid: 3, username: ‘Mike’, password: md5(‘true’), score: [56, 23, 88, 10] };

需要求出Tom所在考场的学生名单, 我们也是直接回调, 先不采用任何异步控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var findDocuments = function(db, collectName, queryCondition, callback) {
var collection = db.collection(collectName);
collection.find(queryCondition).toArray(function(err, docs) {
callback(null, docs);
});
}
//同样嵌套了3次, 出现黑洞!
MongoClient.connect('mongodb://localhost:27017/test', function(err, db) {
findDocuments(db, 'students', {username: 'Tom'}, function (err, result){
findDocuments(db, 'students', {examid: result[0].examid}, function (err, result){
console.log(result);
db.close();
});
});
});

eventproxy 《深入浅出nodejs》4.3 P79

(1) 多类型异步协作, all方法将handler注册到事件组合上. 当注册时多个事件都触发后, 会调用handler执行, 每个事件传递的数据, 将会依照事件名顺序, 传入handler作为参数.

这里以场景一的实现作为示例, 当然这里不是很恰当, 因为是多类型, 不应该只有读取文件, 应为多种互不相关操作(比如读文件与读数据库)的混合.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ep.all('file1', 'file2', 'file3', function (file1, file2, file3) {
// 在所有指定的事件触发后,将会被调用执行
console.log(file1);
console.log(file2);
console.log(file3);
});
fs.readFile('1.txt', 'utf8', function (err, data) {
if (err) throw err;
// 触发'file1'事件, data对应ep.all function中的file1
ep.emit('file1', data);
});
fs.readFile('2.txt', 'utf8', function (err, data) {
if(err) throw err;
ep.emit('file2', data);
});
fs.readFile('3.txt', 'utf8', function (err, data) {
if(err) throw err;
ep.emit('file3', data);
});

(2) 重复异步协作, after方法适合重复操作(读取多个文件, 调用多次数据库), 将handler注册到N次相同事件, 触发数达到N次后handler将会被调用执行, 每次触发的数据将会按触发顺序, 存为数组作为参数传入.

我们还是以场景一作为示例, 这个就非常相符.

1
2
3
4
5
6
7
8
9
10
11
var files = ['1.txt', '2.txt', '3.txt'];
ep.after('got_file', files.length, function (list) {
// 所有文件的内容都存在list数组中
console.log(list);
});
for (var i = 0; i < files.length; i++) {
fs.readFile(files[i], 'utf-8', function (err, content) {
// 触发结果事件
ep.emit('got_file', content);
});
}

(3) 持续型异步协作, tail与all类似, 均为注册到事件组合. all()方法的监听器在满足条件后会执行一次, tail()方法的监听器在满足条件执行一次后, 如果组合事件中某事件再被触发, tail()会以新的数据再次执行.

下面的示例就直接拿文档里的示例了, 朴灵大大写的很棒, 膜拜!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ep = new EventProxy();
ep.tail('tpl', 'data', function (tpl, data) {
// 在所有指定的事件触发后,将会被调用执行
// 每2s, tail会利用tpl, data(最新获取)数据重新执行
});
fs.readFile('template.tpl', 'utf-8', function (err, content) {
ep.emit('tpl', content);
});
setInterval(function () {
db.get('some sql', function (err, result) {
// data事件每2s重新触发一次
ep.emit('data', result);
});
}, 2000);

(4) 异常处理, fail方法侦听了error事件,默认处理卸载掉所有handler,并调用回调函数. done方法, 一旦出现error,默认触发error事件(fail)

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
exports.getContent = function (callback) {
var ep = new EventProxy();
ep.all('tpl', 'data', function (tpl, data) {
callback(null, {
template: tpl,
data: data
});
});
// 添加error handler
ep.fail(callback);
fs.readFile('template.tpl', 'utf-8', ep.done('tpl'));
db.get('some sql', ep.done('data'));
/*
done接受回调函数
fs.readFile('template.tpl', 'utf-8', ep.done(function (content) {
// 无需考虑异常
// 手工emit, 主要用于触发多个事件
ep.emit('tpl1', content1);
ep.emit('tpl2', content2);
}));
fs.readFile('template.tpl', 'utf-8', ep.done('tpl', function (tpl) {
// 将内容更改后,返回即可
return tpl.trim();
}));
*/
};

(5) 异步事件触发, 使用 emitLater && doneLater,

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
var ep = EventProxy.create();
db.check('key', function (err, permission) {
if (err) {
return ep.emitLater('error', err);
}
//异步触发check事件, ep在当前事件循环中监听了所有的事件, 之后的事件循环中才会去触发check事件。
ep.emitLater('check', permission);
//若为ep.emit('check', permission); 有可能 ep.once('check') 还未执行, 即事件还未绑定, 就被触发
});
ep.once('check', function (permission) {
permission && db.get('key', function (err, data) {
if (err) {
return ep.emit('error');
}
ep.emit('get', data);
});
});
ep.once('get', function (err, data) {
if (err) {
retern ep.emit('error', err);
}
render(data);
});
ep.on('error', errorHandler);
//done: 带异常处理的emit, 回调函数可以省略err参数
var ep = EventProxy.create();
db.check('key', ep.doneLater('check'));
ep.once('check', function (permission) {
permission && db.get('key', ep.done('get'));
});
ep.once('get', function (data) {
render(data);
});
ep.fail(errorHandler);

async

非常棒的参考demo!

async主要提供三种方式(以map为例):

完全并行, results汇集了所有callback第二个参数的内容(第一个为err参数).

1
async.map(arr, function(item, callback) {}, function(err, results) {});

让我们再来写一下场景一:

1
2
3
4
5
6
7
8
var fileList = ['1.txt', '2.txt', '3.txt'];
async.map(fileList, function(file, callback) {
fs.readFile(file, 'utf8', function (err, data) {
callback(err, data);
});
}, function(err,results) {
console.log(results.length);
});

执行(Series), results同上, 顺序执行可避免回调黑洞, 但并没有异步?是否可以相邻两步间传递结果?

1
async.mapSeries(arr, function(item, callback) {}, function(err, results) {});

限制并行(Limit), results同上, limit为每次并发限制的数量, limit = 2 即每次并发两个function.

1
async.mapLimit(arr, limit, function(item, callback) {}, function(err, results) {});

存在依赖(Auto), tasks为任务函数列表, 可约定依赖, results同上, concurrency为最大并行task数.

1
async.auto(tasks, [concurrency], function(err, results));

利用async, 让我们来写一下场景二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
async.auto({
connect: function(callback) {
MongoClient.connect('mongodb://localhost:27017/test', function(err, db) {
var collection = db.collection('students');
callback(err, collection);
});
},
// connect后才能执行getExamid
getExamid: ['connect', function(callback, results) {
var collection = results.connect.findOne({username: 'Tom'}, function(err, content) {
callback(null, content);
});
}],
getStudentList: ['getExamid', function(callback, results) {
var collection = results.connect.find({examid: results.getExamid.examid}).toArray(function(err, content){
callback(null, content);
});
}]
}, function(err, results) {
console.log(err);
console.log(results.getStudentList);
});

Promise

核心在于三个状态的转换, Pending、Resolved和Rejected, 只能从Pending -> Resolved(成功), Pending -> Rejected(失败), 不可逆.

ES6 的原生promise来实现一下场景二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var collection;
var connect = new Promise(function(resolve, reject) {
MongoClient.connect('mongodb://localhost:27017/test', function(err, db) {
if(err){
reject(err);
} else {
collection = db.collection('students');
resolve(collection);
}
});
});
var getExamid = function(collection) {
return collection.findOne({username: 'Tom'});
};
var getStudentList = function(result) {
var examid = result.examid;
return collection.find({examid: examid}).toArray();
};
connect.then(getExamid)
.then(getStudentList)
.then(result => console.log(result))
.catch(error => console.log(error));

co

基于 ES6 Generator 的异步解决方案, 让异步在编码中书写如同同步, 同时省去Generator执行器的编写

参考 co 函数库的含义和用法

co 模块来模拟一下场景二:

1
2
3
4
5
6
7
co(function*() {
var db = yield MongoClient.connect('mongodb://localhost:27017/test');
var collection = db.collection('students');
var docs = yield collection.findOne({username: 'Tom'});
docs = yield collection.find({examid: docs.examid}).toArray();
console.log(docs);
});

yield关键字接受方法必须thunk化 – thunkify

小结

node的异步控制, 我觉得是node最基础也是最重要的一部分. 避免回调黑洞, 有利于减少嵌套层数, 便于理解和修改, 也能有效利用node事件驱动、非阻塞I/O的特点.

最开始学习时, 就接触了朴灵大大的《深入浅出nodejs》中异步编程章节, 偏底层的具体实现, 现在回过头去看感觉还是有点困难, js功底还不够, 需要努力了.

之后看了alsotang的《Node.js包教不包会》中lesson4、5, 通过lesson中示例对于异步控制终于有了一些感觉.

重要的事情说三遍! 看文档, 看文档, 看文档! 其实之前一直挺排斥看英文文档的, 容易晕, 但是确实文档全面清晰, 而且带有很好的示例, mongodb driver文档就提供了promise、co的例子, 让我有一种豁然开朗的感觉.

异步控制的学习还远远没有结束, 目前只达到了会用的阶段, 之后我会将我的博客异步重写一遍, 再深入研究一下各个异步控制, 比如co中的generator, ES7中的async等等, 前面的路还很长, 加油!