注:这是一篇学习笔记,记录自己在ES6学习过程中按照自己的思路觉得应该记录的一些要点,方便以后查看和复习。参考:
阮一峰ES6的书籍中ES6 async函数
我的ES6入门学习规划
这篇记录的是异步遍历器相关的东西,这是ES2018加入的新知识。 掌握以下知识内容的前提,还是前面的三大知识:Iterator、Generator函数和async函数。
异步遍历器
ES2018推出了异步遍历器,目前几乎还没有内置对象包含了它的实现。它与同步遍历器是相似的,但是在调用next等实例方法的时候有所区别:同步遍历器直接返回一个含value和done属性的对象,异步遍历器返回一个Promise,必须注册then回调,才能拿到含value和done属性的对象。
异步遍历器有什么作用呢?它专门用于异步的数据结构遍历场景。 同步遍历器部署在Symbol.iterator这个属性上,而异步遍历器部署在Symbol.asyncIterator这个属性上。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
43let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
[Symbol.asyncIterator]() {
let current = 0;
return {
next: ()=>{
return new Promise(resolve=>{
setTimeout(()=>{
resolve({
done: current === this.length,
value: this[current]
});
current+=1;
}, 1000);
});
}
}
}
}
let asyncIterator = someData[Symbol.asyncIterator]();
asyncIterator.next()
.then(data=>{
if(!data.done) {
console.log(data);
return asyncIterator.next();
}
}).then(data=>{
if(!data.done) {
console.log(data);
return asyncIterator.next();
}
}).then(data=>{
if(!data.done) {
console.log(data);
return asyncIterator.next();
}
});
for await of
ES6新推出了for await of结构来用于异步遍历器的遍历,这样就不用自己去手动遍历asyncIterator数据了: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
29let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
[Symbol.asyncIterator]() {
let current = 0;
return {
next: ()=>{
return new Promise(resolve=>{
setTimeout(()=>{
resolve({
done: current === this.length,
value: this[current]
});
current+=1;
}, 1000);
});
}
}
}
}
console.log('start');
for await(let value of someData) {
console.log(value);
}
console.log('end');
for await of结构与其前后的代码执行是同步的,但是它内部的异步遍历器的遍历却是异步的,因为它在内部会自动调用异步遍历器的next方法,并通过返回的Promise实例注册回调来完成下一次遍历,所以这也算一种同步表达异步逻辑的方式。 跟for of循环一样,for await of同样忽略掉了异步遍历器遍历结果的done为true时value值。
注意:只有实现了异步遍历接口的对象才能用于for await of循环,而是否实现了异步遍历接口,取决于对象有没有通过[System.asyncIterator]来部署一个创造异步遍历器的方法;未实现异步遍历接口的数据,用于for await of循环,将会报错。
如果异步遍历器的next方法返回的Promise被reject,或者异步遍历器遍历过程中抛出错误,那么用于for await of循环时,会抛出错误。 (以下有两个举例)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
41let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
[Symbol.asyncIterator]() {
let current = 0;
return {
next: ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
if(current == 1) {
reject(new Error('sth bad happened'));
} else {
resolve({
done: current === this.length,
value: this[current++]
});
}
}, 1000);
});
}
}
}
}
console.log('start');
try {
for await(let value of someData) {
console.log(value);
}
} catch(e) {
console.log('捕获到遍历错误', e);
}
console.log('end');
// start
// tom
// 捕获到遍历错误 Error: sth bad happened
// at <anonymous>:13:15
// end
1 | let someData = { |
for await of循环可正常使用break continue这些语法控制遍历。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
30let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
[Symbol.asyncIterator]() {
let current = 0;
return {
next: ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve({
done: current === this.length,
value: this[current++]
});
}, 1000);
});
}
}
}
}
let c = 0;
for await(let value of someData) {
if(c == 1) break;
c++;
console.log(value);
}
// tom
这个例子只打印了一个tom,后面的没有处理。
异步遍历器的return方法
应用于for await of循环有break
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
35let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
[Symbol.asyncIterator]() {
let current = 0;
return {
next: ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve({
done: current === this.length,
value: this[current++]
});
}, 1000);
});
},
return: ()=>{
console.log('ret....');
return {done: true};
}
}
}
}
let c = 0;
for await(let value of someData) {
if(c == 1) break;
c++;
console.log(value);
}
// tom
// ret....应用于for await of循环使用时发生错误
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
36let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
[Symbol.asyncIterator]() {
let current = 0;
return {
next: ()=>{
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve({
done: current === this.length,
value: this[current++]
});
}, 1000);
});
},
return: ()=>{
console.log('ret....');
return {done: true};
}
}
}
}
let c = 0;
for await(let value of someData) {
if(c == 1) throw new Error('for ex');
c++;
console.log(value);
}
// tom
// ret....
// Uncaught (in promise) Error: for ex Error: for ex
异步遍历器的return方法,目前来看,与同步遍历器的return方法,都是在for结构有错或有break时会被调用。两者的区别是有的:异步遍历器的return方法实际上应该返回的是一个Promise实例,如果不是,也会被Promise.resolve处理;同步遍历器的return方法只要返回一个普通对象即可。 看到后面的异步Generator函数的return方法就知道了。
异步遍历器与同步遍历器的差异
- 异步遍历器的next方法返回的是Promise,而同步遍历器的next方法直接返回本次遍历的结果对象;
- 异步遍历器需要部署到[System.asyncIterator]属性上,而同步遍历器需要部署到[System.iterator]属性上;
- 异步遍历器使用for await of来遍历,而同步遍历器使用for of循环来遍历;
异步Generator函数
前面加async关键字的Generator函数,就是所谓的异步Generator函数。 相比同步Generator函数,异步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
26
27
28
29
30let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
async * [Symbol.asyncIterator]() {
yield new Promise(resolve=>{
setTimeout(()=>{
resolve(this[0]);
}, 1000);
});
yield new Promise(resolve=>{
setTimeout(()=>{
resolve(this[1]);
}, 1000);
});
yield new Promise(resolve=>{
setTimeout(()=>{
resolve(this[2]);
}, 1000);
});
}
}
console.log('start');
for await(let value of someData) {
console.log(value);
}
console.log('end');
异步Generator函数与同步Generator函数的执行规则是完全相同的。 所以只要了同步Generator函数的执行原理,再把异步的规则套进去,就能理解异步Generator函数。
yield是一个关键词,它后面可以接表达式,它的作用是将异步Generator函数体从起始位置到结束位置(比如:return)分为多个阶段,如果把函数体看作是一条线段,每一个yield表达式就是这条线段中的一个分隔点,函数体的起始位置和结束位置也是这条线段上的点,这些点的含义代表了函数体执行时的位置;通过调用异步Generator函数返回的异步Iterator对象的next方法,可以让Generator函数体真正开始执行,并且是分阶段执行。详细的规则如下:
- 每一次调用异步Iterator对象next方法,都会让函数体从当前的执行位置执行到下一个yield关键词出现的位置,并把yiled后面表达式的值,组装成一个{done,value}对象,fulfill掉next方法返回的Promise实例,Generator函数的执行就会停在这里;函数体的默认执行位置是函数体的起始位置;
- 如果调用next方法时,函数体当前执行位置后面没有了yield 表达式,就会把剩下的代码都执行完,并把函数返回值组装成一个{done,value}对象,fulfill掉next方法的返回的Promise实例,至此一个Generator函数才算是完整的运行结束;
- 当函数体已经运行结束,继续调用next方法不会再继续运行函数体内的代码,都会得到一个fulfilled的Promise实例。
1 | async function * makeAsyncIterator() { |
跟预想的稍有区别的是,异步Generator函数的函数体运行完以后,对它返回的异步Iterator对象,继续调用next方法并注册回调,始终拿到是一个{done: true, value: undefined}的对象,而不是函数体的最后一次遍历结果。
从上面的例子也可以看到,虽然异步Generator函数要求返回异步Iterator对象,意味着与next方法对应的yield关键字后面的表达式的值应该是一个Promise实例,但实际上不是这么严格,它可以是任意类型数据,异步Generator函数会将yield关键字后面表达式的值通过Promise.resolve处理一下,转变为Promise实例。
next方法也可以传入参数,成为当前执行位置的yield表达式的返回值,这跟同步的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
26
27
28async function* foo(x) {
console.log("x", x);
var y = 2 * (yield (x + 1));
console.log("y", y);
var z = yield (y / 3);
console.log("z", z);
return (x + y + z);
}
var gen = foo(5);
gen.next()
.then(data=>{
console.log(data);
return !data.done && gen.next(12);
})
.then(data=>{
console.log(data);
return !data.done && gen.next(13);
})
.then(data=>{
console.log(data);
})
// x 5
// {value: 6, done: false}
// y 24
// {value: 8, done: false}
// z 13
// {value: 42, done: true}
next方法的参数会被传入到Generator函数体内,作为当前执行位置的yield表达式的返回值,但是第一次调用next方法传递的参数是无效的,因为此时函数体的执行位置是起始位置,没有yield表达式。通过上面例子中的log可以清晰地看到yield后面表达式的返回值以及通过next方法注入数据后,yield表达式的返回值。
异步Generator函数的throw方法
应该与同步Generator函数是保持一致的。 此处并没有去做验证,可参考:ES6 Generator函数的语法
异步Generator函数的return方法
1 | var genFunc = async function* () { |
异步Generator函数返回的异步Iterator对象的return方法,返回的也是一个Promise实例,它的其它特性与同步Generator函数应该是一致的。此处并没有去做验证,可参考:ES6 Generator函数的语法
yield *表达式
引用同步Generator函数中yield *表达式的作用:
它的作用实际上就是把它后面的Iterator对象遍历逻辑并入到当前的Generator函数中,并把它后面的Iterator对象遍历完成时的value值作为yield*表达式的返回值。 再以线段来类比Generator函数执行的话,yield *表达式等同于给Generator函数增加了额外的分隔点。
yield *表达式在异步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
26
27async function *inner(){
yield 2;
console.log('before inner return');
return 3;
}
async function *outer(){
yield 1;
let ret = yield* inner();
console.log('after inner return');
yield ret;
let ret2 = yield * [4, 5];
return 6;
}
let gen = outer();
for await (let v of gen) {
console.log(v);
}
// 1
// 2
// before inner return
// after inner return
// 3
// 4
// 5
yield * 后面如果接的是另外一个异步Generator函数,那么这个函数的返回值,会作为yield *表达式的返回值。
总之yield * 表达式不管是在同步Generator函数还是异步Generator函数中使用,它的特性都是一致的。 更多可参考:ES6 Generator函数的语法
异步Generator函数中的async函数特征
异步Generator函数核心是Generator函数,但是具有async函数特性:
- 前面有async关键字
- 内部可以使用await关键字,await后面接的表达式,跟在async函数中一样,接异步任务;
这使得异步Generator函数体在执行的时候,await关键字的特性,与在async函数中一模一样。 也就是说虽然await后面是异步任务,但是在异步Generator函数体中,只有await后面的异步任务结束了,await所在语句才能执行完。 所以异步Generator函数在执行的时候,与async函数有点相似性。 可以把异步Genrator函数看作是同步Generator函数与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
30let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
async * [Symbol.asyncIterator]() {
yield await new Promise(resolve=>{
setTimeout(()=>{
resolve(this[0]);
}, 1000);
});
yield await new Promise(resolve=>{
setTimeout(()=>{
resolve(this[1]);
}, 1000);
});
yield await new Promise(resolve=>{
setTimeout(()=>{
resolve(this[2]);
}, 1000);
});
}
}
console.log('start');
for await(let value of someData) {
console.log(value);
}
console.log('end');
小结
这篇笔记学习到的异步遍历器和异步Generator函数,总的来说,与前面的两个相似知识,有很多相通的特性,所以学习和使用的时候,还得结合需要,多研究才行,尽可能地参考同步Generator函数和同步遍历器。 async函数是Generator函数的语法糖,能够让Generator函数自动运行,现在异步的Generator函数还没有推出类似的语法糖,所以如果要自动运行异步的Generator函数,就必须地靠自己了。 从实际的一些需求来看,目前也没有发现啥可去使用异步Generator函数和异步遍历器的场景,网上对这块的学习分享也不多,所以这个知识点应用地范围比较小。 目前浏览器对它的支持度不高,mdn上可查。