简介
就像 Generator
函数返回一个同步遍历器对象一样,异步 Generator
函数,返回一个异步遍历器。
一个简单的例子
async function* gen() {
yield 'hello';
yield 'world';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }
genObj.next().then(x => console.log(x));
// { value: 'world', done: false }
异步 Generator
函数在写法上与同步 Generator
函数仅仅相差了 async
关键字,最主要的区别是在 next
方法的调用上,同步 Generator
函数调用 next
方法返回一个 { value: any, done: boolean }
的对象,而异步 Generator
函数返回一个 Promise
对象,其异步操作的结果为 { value: any, done: boolean }
。
为了方便称呼,异步 Generator
函数称为 async Generator
,同步 Generator
函数为 Generator
。
之前在 Generator
函数中提到过,该函数的作用之一为:部署在 Iterator
接口上,实现数据的遍历。async Generator
函数同样的也可以部署在一个专门用于异步遍历的接口 asyncIterator
,那上面是 asyncIterator
接口?。
asyncIterator 接口
异步遍历器的最大的语法特点,就是调用遍历器的 next
方法,返回的是一个 Promise
对象。
一个简单的符合 asyncIterator
接口函数:
function createAsyncIterable(arr){
let arrCopy = [...arr];
return {
next(){
return Promise.resolve({
value: arrCopy.pop(),
done: arrCopy.length === 0
});
}
}
}
let ai = createAsyncIterable(['hello', 'world'])
ai.next().then(x => console.log(x));
// { value: 'hello', done: false }
ai.next().then(x => console.log(x));
// { value: 'world', done: true }
形式上与同步 Iterator
保持较高程度的一致性,不同点仅有一点:next
方法不直接返回数据,而是返回 Promise
,所以数据的获取应该在 then
方法中,并且 Promise
中返回的数据应该与同步的一致: { value: any, done: true }
。
异步遍历器的简单使用:
let ai = createAsyncIterable(['hello', 'world'])
// 直接使用
ai.next();
.then(iterResult1 => {
console.log(iterResult1);
// { value: 'hello', done: false }
// 获取下一次遍历的 Promise;
return ai.next();
})
.then(iterResult2 => {
console.log(iterResult2);
// { value: 'world', done: true }
})
// 使用 async/await
async function f() {
let ai = createAsyncIterable(['hello', 'world']);
console.log(await ai.next());
// { value: 'hello', done: false }
console.log(await ai.next());
// { value: 'hello', done: true }
}
方式一使用 Promise
一步步调用,但是这种写法带着一股浓浓的异步风采。
第二种使用 async/await
很明显这已经和同步写法差不多了,所以较为推荐使用第二种写法,去遍历异步 Iterator
接口。
for await...of
之前说到过,为了配合 Iterator
接口,ES6
规定了一种新的遍历方式 for...of
循环,同样的为了配合 asyncIterator
接口,规定了 for await...of
循环。
一个例子:
// asyncIterator 接口部署在对象的 Symbol.asyncIterator 属性上
let obj = {
arr: ['world', 'hello'],
[Symbol.asyncIterator](){
let arrCopy = [...this.arr]
return {
next(){
return Promise.resolve({
done: arrCopy.length === 0,
value: arrCopy.pop()
});
}
}
}
}
async function f() {
for await (const x of obj) {
console.log(x);
}
}
f();
// hello
// world
如果还记得同步遍历器的编写方式的话,相信看到这些代码会很眼熟,区别仅仅只有两点
- 接口名称为:
Symbol.asyncIterator
- 使用
for await...of
循环,还有函数前的async
可能这么说不会觉的异步遍历器的强大,一个简单的例子:
Node v10
支持异步遍历器,Stream
就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。
// 传统写法
function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
}
// 异步遍历器写法
async function main(inputFilePath) {
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
for await (const chunk of readStream) {
console.log('>>> '+chunk);
}
console.log('### DONE ###');
}
传统方式使用事件的形式来传输数据,而异步遍历器的写法更加的直观,可以很明显的看出不仅仅代码简洁了,流程也更加的清楚明了。
asyncIterator 与 async Generator
asyncIterator
与 async Generator
的关系和 Iterator
与 Generator
的关系是一致的,前者规定了一个接口的形式,后者为前者产出规定形式的接口。而在异步操作中,我们一般不会自己去写符合 asyncIterator
的函数,直接使用 async Generator
更方便快捷。
一个简单的例子
function createPromise(params){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(params)
}, 1000)
})
}
let ag = async function* gen(){
yield createPromise('hello')
yield createPromise('world')
}
let obj = {
arr: ['world', 'hello'],
[Symbol.asyncIterator]: ag
}
async function f() {
for await (const x of obj) {
console.log(x);
}
}
f();
// hello
// world
将这段代码运行,可以发现 1
秒后输出 hello
接着 1
秒后输出 world
。不仅代码简洁,而且执行顺序也是我们想要的。
总结
asyncIterator
与 async Generator
的配合使用使我们能更好的去控制有顺序的且结构一致的异步循环,但我们始终要明确一点:这一切都是基于 Promise
实现了,不论流程上多么的清晰明了,这和 async/await
一样,都仅仅是一块语法糖,javascript
中是不存在正真的异步同步化,这点需要牢记。