前端一般都不会对JavaScript中的异步回调这个概念感到陌生,对Callback Hell这个名词更是再熟悉不过。为了实现异步的效果,我们只能通过传入回调函数进行后续的操作,而一旦出现各操作依次依赖,就会出现Callback Hell,具体而言,如下:

1
2
3
4
5
6
7
doAsync1(function () {
doAsync2(function () {
doAsync3(function () {
doAsync4(function () {
})
})
})

显然,这种代码不仅难看不易理解,而且还不利于重构,为此,业界提出了诸多解决方案来避免这样的问题。而基于 generator 的 co 可以说是一个比较优雅的方案。这里分析一下如何使用 generator 解决回调问题。

generator是ES6提供的一个最新特性,它允许我们在函数执行的过程中提前返回并在下次调用的时候从返回点继续,一个基本的用法如下:

1
2
3
4
5
6
7
8
9
10
11
function* gen() {
var a = yield 'Hello';
console.log('--', a);
var b = yield 'World';
console.log('--', b);
return a + b;
}
var fn = gen();
console.log(fn.next()); // 第一次调用,启动函数体,在yield处停止,返回的是yield后执行的内容,即'Hello'
console.log(fn.next('X')); // 调用next传入的参数会替换yield处的内容赋值给a,同时继续往下执行,return 'World', a = 'Y'
console.log(fn.next('Y')); // 调用next传入的参数会替换yield处的内容赋值给b,return XY

为了利用这个特性解决异步回调,定义如下函数:

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
var fs = require('fs');
function wrap(fn) { // 将普通异步函数包装成适配 generator 的函数
return function() {
var args = [].slice.call(arguments);
var callback;
args.push(function() {
if (typeof callback === 'function') {
callback.apply(null, arguments);
}
});
fn.apply(null, args);
return function(cb) {
callback = cb;
};
};
}
var co = function(gen) {
gen = gen();
var next = function(data) {
var ret = gen.next(data); // 返回的是readFile的结果,即function (cb) {callback = cb;}
if (ret.done) {
return;
}
if (Array.isArray(ret.value)) {
var count = 0,
results = [];
ret.value.forEach(function(fn, index) {
count += 1;
fn(function(err, data) {
count -= 1;
if (err) {
throw err;
}
results[index] = data;
if (count === 0) {
next(results);
}
});
});
} else {
ret.value(function(err, data) {
if (err) {
throw err;
}
next(data);
});
}
}
next();
};
function sleep(ms, resolve) {
setTimeout(resolve, ms);
}
var sleep = wrap(sleep);
function* flow() {
console.time('parallel-yield');
console.log('start parallel yield');
yield [sleep(2000), sleep(1000), sleep(50), sleep(20)];
console.timeEnd('parallel-yield');
console.time('yield-one-by-one');
console.log('start yield one by one');
yield sleep(2000);
yield sleep(1000);
console.timeEnd('yield-one-by-one');
}
try {
co(flow);
} catch (e) {
console.error(e);
}
// 调用readFile以后,会返回一个函数,接收一个回调函数作为它的参数,当异步触发的
// 时候,就会调用这个回调函数
/*
var readFile = wrap(fs.readFile);
var flow = function* () {
var txt = yield readFile('filename.txt', 'utf8'); // 或者我们可以写成并行的方式 var [txt, content] = [readFile('filename.txt'), readFile('str.js', 'utf8')];
var content = yield readFile('str.js', 'utf8');
console.log(txt + content);
};
try {
co(flow);
} catch(e) {
console.error(e);
}
*/