精通异步编程

本文主要用下面几个例子来讲解异步编程:

异步串行执行例子(有参数依赖,比较常见):
①遍历文件夹下文件;②根据遍历读取文件夹下文件的内容;③根据读取的内容操作数据库。

异步串行执行例子(无参数依赖,比较少见,一般无参数依赖都是并发执行较多):
①设置数据库数据;②获取数据库数据。

异步并发执行例子:
①获取模板;②获取数据;③获取资源文件。

一、callback方式

异步串行执行:

1
2
3
4
5
6
7
8
9
fs.readdir(path.join(__dirname, '..'), (err, files) => {
files.forEach((filename, index) => {
fs.readFile(filename, 'utf-8', (err, file) => {
db.findOne({name: file.name}, (err, doc) => {
// TODO
});
})
})
});

对于异步串行执行,上面的callback方式也挺清晰合理。但是对于异步并发执行,如果还用嵌套方式,就没有利用到异步性能上的优势。

异步并发执行(哨兵变量方式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let template;
let data;
let resources;
let count = 0;
fs.readFile('template', 'utf-8', (err, file) => {
count++;
resolveResult(file);
});
db.findOne({name: 'xxx'}, (err, doc) => {
count++;
resolveResult(file);
});
l10n.get((err, resources) => {
count++;
resolveResult(file);
});
function resolveResult() {
if (count === 3) {
// TODO;
}
}

优化上面的代码,可以增加一个done函数来判断是否完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let results = {};
let count = 0;
function done(key, value) {
results[key] = value;
count ++;
if (count === 3) {
// TODO;
}
}
fs.readFile('template', 'utf-8', (err, template) => {
done('template', template);
});
db.findOne({name: 'xxx'}, (err, data) => {
done('data', data);
});
l10n.get((err, resources) => {
done('resources', resources);
});

还可以利用偏函数来优化上面的代码,可以把哨兵变量以及result全局变量去掉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function after(times, callback) {
let count = 0;
let results = {};
return function(key, value) {
results[key] = value;
count ++;
if (times === count) {
callback(results);
}
}
}
let done = after(3, (results) => {
// TODO
});
fs.readFile('template', 'utf-8', (err, template) => {
done('template', template);
});
db.findOne({name: 'xxx'}, (err, data) => {
done('data', data);
});
l10n.get((err, resources) => {
done('resources', resources);
});

二、事件发布/订阅方式

在业务继续增长的时候,我们可以使用事件发布/订阅的方式实现多对一的目的:

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
const emitter = new events.Emitter();
function after(times, callback) {
let count = 0;
let results = {};
return function(key, value) {
results[key] = value;
count ++;
if (times === count) {
callback(results);
}
}
}
let done = after(3, (results) => {
// TODO
});
emitter.on("done", done);
emitter.on("done", other);
fs.readFile('template', 'utf-8', (err, template) => {
emitter.emit('done', 'template', template);
});
db.findOne({name: 'xxx'}, (err, data) => {
emitter.emit('done', 'data', data);
});
l10n.get((err, resources) => {
emitter.emit('done', 'resources', resources);
});

这里推荐一个利用事件发布/订阅方式写的模块(EventProxy)[https://github.com/JacksonTian/eventproxy]

三、Promise方式

异步串行执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let p1 = fs.readdir(path.join(__dirname, '..'), (err, files) => {
if (err) reject(err);
resolve(files);
});
let p2 = function(files, cb) {
let plist = [];
files.forEach((filename, index) => {
let p = fs.readFile(filename, 'utf-8', (err, file) => {
db.findOne({name: file.name}, (err, doc) => {
// TODO
});
})
plist.push(p);
});
return Promise.all(plist);
}
p1.then(files=>{
p2(files).then(()=>{
// TODO;
});
})

异步并发执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let p1 = new Promise((resolve, reject) => {
fs.readFile('template', 'utf-8', (err, template) => {
if (err) reject(err);
resolve(template);
});
});
let p2 = new Promise((resolve, reject) => {
db.findOne({name: 'xxx'}, (err, data) => {
if (err) reject(err);
resolve(data);
});
});
let p3 = new Promise((resolve, reject) => {
l10n.get((err, resources) => {
if (err) reject(err);
resolve(resources);
});
});
Promise.all([p1,p2,p3]).then((results) => {
// TODO
}).catch((err)=>{
console.log(err);
})

四、async流程控制库

用async库可以很好的解决异步流程控制的问题,而且适用于浏览器端,地址:https://caolan.github.io/async/

异步串行执行(有参数依赖):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async.waterfall([
(callback) => {
fs.readdir(path.join(__dirname, '..'), (err, files) => {
callback(err, files);
})
},
(files, callback) => {
files.forEach((filename, index) => {
fs.readFile(filename, 'utf-8', (err, file) => {
db.findOne({name: file.name}, (err, doc) => {
// TODO
});
})
})
}
], (err, results) => {
// TODO
});

异步串行执行(无参数依赖):

1
2
3
4
5
6
7
8
9
10
async.series([
(callback) => {
db.create({name: 'xxx'}, callback);
},
(callback) => {
db.findOne({name: 'xxx'}, callback);
}
], (err, results) => {
// TODO
})

异步并发执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
async.parallel([
(callback) => {
fs.readFile('template', 'utf-8', callback);
},
(callback) => {
db.findOne({name: 'xxx'}, callback);
},
(callback) => {
l10n.get(callback);
}
], (err, results) => {
// TODO
})

五、生成器 + Promise + co

生成器函数在执行时能中途退出,后面又能重新进入继续执行。而且在函数内定义的变量的状态都会保留,不受中途退出的影响。
co模块是TJ大神写的生成器自动执行模块,通过将生成器传入co函数中来自动执行,地址:https://github.com/tj/co

异步串行执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
co(function* () {
let files = yield new Promise((resolve, reject) => {
fs.readdir(path.join(__dirname, '..'), (err, files) => {
if (err) reject(err);
resolve(files);
});
});
let p2 = function(files) {
let plist = [];
files.forEach((filename, index) => {
let p = fs.readFile(filename, 'utf-8', (err, file) => {
db.findOne({name: file.name}, (err, doc) => {
// TODO
});
});
plist.push(p);
})
return Promise.all(plist);
}
})

异步并发执行:

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
co(function* () {
let p1 = new Promise((resolve, reject) => {
fs.readFile('template', 'utf-8', (err, template) => {
if (err) reject(err);
resolve(template);
});
});
let p2 = new Promise((resolve, reject) => {
db.findOne({name: 'xxx'}, (err, data) => {
if (err) reject(err);
resolve(data);
});
});
let p3 = new Promise((resolve, reject) => {
l10n.get((err, resources) => {
if (err) reject(err);
resolve(resources);
});
});
let template = yield p1;
let data = yield p2;
let resources = yield p3;
// TODO
});

六、async + await + promisify

这种方式比生成器更好的地方在于可以省略掉co模块,直接以同步的方式写异步代码,但是需要将所有的异步操作的改造成promise方式,所以Node 8中有了util.promisify模块,使用起来也非常方便。

异步串行执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const promisify = require('util.promisify');
const readdir = promisify(fs.readdir);
const readFile = promisify(fs.readFile);
const findOne = promisify(db.findOne);
async function foo() {
let files = await readdir(path.join(__dirname, '..'));
files.forEach((filename, index) => {
readFile(filename, 'utf-8').then((file) => {
findOne({name: file.name})
});
});
}
foo().then(() => {
// TODO
})

异步并发执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const promisify = require('util.promisify');
const readFile = promisify(fs.readFile);
const findOne = promisify(db.findOne);
const l10nget = promisify(l10n.get);
async function foo() {
let getTemplate = readFile('template', 'utf-8');
let getData = findOne({name: 'xxx'});
let getResources = l10nget();
let template = await getTemplate;
let data = await getData;
let resources = await getResources;
return {
template: template,
data: data,
resources: resources
}
}
foo().then(results => {
// TODO
});

总结:如果在Nodejs端,并且版本使用的8以上,那么推荐使用async+await+promisify方式;如果Nodejs版本低于8,可以使用生成器+Promise+co的方式;如果是不支持生成器和前端浏览器下,推荐使用async流程控制库。

分享到