ES6 异步遍历器与异步Generator函数

注:这是一篇学习笔记,记录自己在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
43
let 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
29
let 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
41
let 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
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
let someData = {
0: 'tom',
1: 'jerry',
2: 'dog',
length: 3,
[Symbol.asyncIterator]() {
let current = 0;
return {
next: ()=>{
if(current == 1) throw new Error('another ex happened');
return new Promise((resolve,reject)=>{
setTimeout(()=>{
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: another ex happened
// at <anonymous>:13:15
// end

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
30
let 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
    35
    let 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
    36
    let 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方法就知道了。

异步遍历器与同步遍历器的差异

  1. 异步遍历器的next方法返回的是Promise,而同步遍历器的next方法直接返回本次遍历的结果对象;
  2. 异步遍历器需要部署到[System.asyncIterator]属性上,而同步遍历器需要部署到[System.iterator]属性上;
  3. 异步遍历器使用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
30
let 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函数体真正开始执行,并且是分阶段执行。详细的规则如下:

  1. 每一次调用异步Iterator对象next方法,都会让函数体从当前的执行位置执行到下一个yield关键词出现的位置,并把yiled后面表达式的值,组装成一个{done,value}对象,fulfill掉next方法返回的Promise实例,Generator函数的执行就会停在这里;函数体的默认执行位置是函数体的起始位置;
  2. 如果调用next方法时,函数体当前执行位置后面没有了yield 表达式,就会把剩下的代码都执行完,并把函数返回值组装成一个{done,value}对象,fulfill掉next方法的返回的Promise实例,至此一个Generator函数才算是完整的运行结束;
  3. 当函数体已经运行结束,继续调用next方法不会再继续运行函数体内的代码,都会得到一个fulfilled的Promise实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function * makeAsyncIterator() {
yield 1;
yield 2;
return 3;
}

let asyncIterator = makeAsyncIterator();
asyncIterator.next()
.then(data=>{
console.log(data);
return !data.done && asyncIterator.next();
})
.then(data=>{
console.log(data);
return !data.done && asyncIterator.next();
})
.then(data=>{
console.log(data);

asyncIterator.next().then(data=>{
console.log('keep',data);
});
});

跟预想的稍有区别的是,异步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
28
async 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var genFunc = async function* () {
yield 'state1';
yield 'state2';
return 'state3';
};

var gen = genFunc();
gen.next()
.then(data=>{
console.log(data);

gen.return('no state').then(data=>{
console.log(data);
});
})

异步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
27
async 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函数特性:

  1. 前面有async关键字
  2. 内部可以使用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
30
let 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上可查。