Javascript异步编程

随着es6的发布和Babel的出现,对于异步编程,我们慢慢告别了之前回调地狱的问题,现在的我们有了更多的选择,例如:Promise, generator, async/await等方案。下面我们一一来了解一下:

###Promise
Promise在ES6发布之前就已经有很多的库和工具实现了这个功能,例如Jquery,p等等, Promise做到了将原来的回调地狱的方式,改为了链式返回,在一定程度上改善了异步编程的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var p1 = new Promise(function (resolve, reject) {
setTimeout(function() {
console.log('resolve');
resolve('done');
}, 1000);
});

p1.then(function(data){
// success
console.log(data)
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});

###Generator
Generator是ES6中新增的语法,Generator可以做到在函数执行过程中停止和继续进行,从而使异步函数可以以同步编程的方式编写,在异步的地方停止,在异步函数执行完后继续执行Generator函数。

在调用generator函数时,并不会执行函数内容,而是会返回一个迭代器(iterator)对象, 在执行迭代器的next()函数(next()函数是iterator的属性)时可以逐步执行generator函数内容。

注:一个对象要想可以生成迭代器,该对象需要有“Symbol.iterator”属性

1
2
3
4
5
6
7
8
9
10
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result);
yield 1;
}

//在chrome控制台中输出
> gen.prototype[Symbol.iterator]
//function [Symbol.iterator]() { [native code] }

让我们运行 gen()

1
2
3
4
5
6
7
8
9
10
11
12
13
var g = gen();

var a = g.next();

console.log(a);//{value: Promise, done: false}

a = g.next();

console.log(a);//{value: 1, done: false}

a = g.next();

console.log(a);//{value: undefined, done: true}

对于上面的结果,我们也可以参看babel对于Generator的实现,不过这并不是官方的方案,只是可以作为理解Generator的一个帮助

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
function gen() {
var url, result;
return regeneratorRuntime.wrap(function gen$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
url = 'https://api.github.com/users/github';
_context.next = 3;//设置下一个起始点
return fetch(url);//输出yield后面的表达式

case 3:
result = _context.sent;

console.log(result);
_context.next = 7;//设置下一个起始点
//输出yield后面的表达式
return 1;

case 7:
case 'end':
return _context.stop();
}
}
}, _marked[0], this);
}

由此,我们也可以猜测调用Generator的时候,调用了gen.prototype[Symbol.iterator],该函数相当于是把我们的代码根据标签yield进行分割,存储在一个“列表”中,并返回了一个迭代器,其中含有next属性,虽随着next的调用,一步步地执行“列表”中的每一段函数。

当我们运行的时候会发现上面的console.log(result);输出了undefined,这是为什么呢,因为Generator只是负责代码的停止和执行,它并不会等待后面的异步结束,而且一步步的写next太麻烦了,能不能让Generator自己执行到结束呢?

当然可以!

但是只有Generator并不能做到这些,因为Generator不知道异步代码什么时候结束,但是Promise知道啊,所以我们还需要上面所说的Promise。

为了方便大家理解,我们一步步来,我们先实现Generator的自执行,在每次调用next()时,都会返回一个对象,包含yield后面语句的执行的返回值和一个是否完成全部Generator函数的标记done,我们可以不断调用next直到done === true为止,就完成了Generator的自执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* gen(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}

function run(fn) {
var gen = fn();

function next(data) {
var result = gen.next(data);
if (result.done) return;
next(result);
}

next();
}

run(gen);

接下来我们需要在上面的基础上引入Promise,现在我们的run函数中一个我们自己写的next函数,显而易见的,我们只需要稍微改动一下这个函数即可。为了方便,我们还写了一个readFile函数模仿异步代码,它会返回一个Promise并在1000ms后resolve。

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 readFile = function(filename) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve(filename);
}, 1000);
});
}

function* gen() {
var a = yield readFile('1');
console.log(a);
a = yield readFile('2');
console.log(a);
a = yield readFile('3');
console.log(a);
}

var run = function(gen) {
var g = gen();

function next(err, data) {
var result = g.next(data);
if (!result.done) {
result.value.then(function(data) {
next(null, data);
return data;
});
} else {
console.log('done');
}
}
next();
};

run(gen);

上面的run函数有一个问题,现在我们默认yield后面一定是一个返回Promise的函数,那很多时候不一定会是这样,所以我们还需要一个转换器,把一切的对象都转为Promise,这个我们这里不再实现,具体可以参看TJ大神写的co

async/await

在ES6的基础上,ES2017提供了async函数,这将得异步操作变得更加方便。我们把我们上面的gen函数用async/await改写一下。其实就是去除*,加上async, 所有的yield改为await就好了

1
2
3
4
5
6
7
8
9
10
11

async function gen() {
var a = await readFile('1');
console.log(a);
a = await readFile('2');
console.log(a);
a = await readFile('3');
console.log(a);
}

gen()

上面的代码会和上一节我们最后的代码结果一样,这是得益于async自带有自执行和异步等待。我们就不用再写自己的自执行函数和异步代码等待的逻辑了。await命令后面,可以是Promise对象,也可以是其他原始类型(数值、字符串和布尔值,数组等),是不是很酷炫。

另外不同Generator返回一个迭代器,async函数返回一个Promise对象

所以上面的例子我们还可以加上

1
2
3
gen().then(function(){
console.log('123')
})

async/await很酷,但是现在浏览器还是没有完全支持,Node端只有7.0以上才可以使用,所以还是要使用Babel。

结束:
随着ES6的发布,我们的异步代码方案也越来越趋于成熟了