写点什么

ES9 的新特性: 异步遍历 Async iteration

发布于: 2021 年 04 月 15 日

简介在 ES6 中,引入了同步 iteration 的概念,随着 ES8 中的 Async 操作符的引用,是不是可以在一异步操作中进行遍历操作呢?


今天要给大家讲一讲 ES9 中的异步遍历的新特性 Async iteration。


异步遍历在讲解异步遍历之前,我们先回想一下 ES6 中的同步遍历。


根据 ES6 的定义,iteration 主要由三部分组成:


Iterable 先看下 Iterable 的定义:


interface Iterable {Symbol.iterator : Iterator;}Iterable 表示这个对象里面有可遍历的数据,并且需要实现一个可以生成 Iterator 的工厂方法。


Iteratorinterface Iterator {next() : IteratorResult;}可以从 Iterable 中构建 Iterator。Iterator 是一个类似游标的概念,可以通过 next 访问到 IteratorResult。


IteratorResultIteratorResult 是每次调用 next 方法得到的数据。


interface IteratorResult {value: any;done: boolean;}IteratorResult 中除了有一个 value 值表示要获取到的数据之外,还有一个 done,表示是否遍历完成。


下面是一个遍历数组的例子:


const iterable = ['a', 'b'];const iterator = iterableSymbol.iterator;iterator.next(){ value: 'a', done: false }iterator.next(){ value: 'b', done: false }iterator.next(){ value: undefined, done: true }但是上的例子遍历的是同步数据,如果我们获取的是异步数据,比如从 http 端下载下来的文件,我们想要一行一行的对文件进行遍历。因为读取一行数据是异步操作,那么这就涉及到了异步数据的遍历。


加入异步读取文件的方法是 readLinesFromFile,那么同步的遍历方法,对异步来说就不再适用了:


//不再适用 for (const line of readLinesFromFile(fileName)) {console.log(line);}也许你会想,我们是不是可以把异步读取一行的操作封装在 Promise 中,然后用同步的方式去遍历呢?


想法很好,不过这种情况下,异步操作是否执行完毕是无法检测到的。所以方法并不可行。


于是 ES9 引入了异步遍历的概念:


可以通过 Symbol.asyncIterator 来获取到异步 iterables 中的 iterator。异步 iterator 的 next()方法返回 Promises 对象,其中包含 IteratorResults。


所以,我们看下异步遍历的 API 定义:


interface AsyncIterable {Symbol.asyncIterator : AsyncIterator;}interface AsyncIterator {next() : Promise<IteratorResult>;}interface IteratorResult {value: any;done: boolean;}我们看一个异步遍历的应用:


const asyncIterable = createAsyncIterable(['a', 'b']);const asyncIterator = asyncIterableSymbol.asyncIterator;asyncIterator.next().then(iterResult1 => {console.log(iterResult1); // { value: 'a', done: false }return asyncIterator.next();}).then(iterResult2 => {console.log(iterResult2); // { value: 'b', done: false }return asyncIterator.next();}).then(iterResult3 => {console.log(iterResult3); // { value: undefined, done: true }});其中 createAsyncIterable 将会把一个同步的 iterable 转换成一个异步的 iterable,我们将会在下面一小节中看一下到底怎么生成的。


这里我们主要关注一下 asyncIterator 的遍历操作。


因为 ES8 中引入了 Async 操作符,我们也可以把上面的代码,使用 Async 函数重写:


async function f() {const asyncIterable = createAsyncIterable(['a', 'b']);const asyncIterator = asyncIterableSymbol.asyncIterator;console.log(await asyncIterator.next());// { value: 'a', done: false }console.log(await asyncIterator.next());// { value: 'b', done: false }console.log(await asyncIterator.next());// { value: undefined, done: true }}异步 iterable 的遍历使用 for-of 可以遍历同步 iterable,使用 for-await-of 可以遍历异步 iterable。


async function f() {for await (const x of createAsyncIterable(['a', 'b'])) {console.log(x);}}// Output:// a// b 注意,await 需要放在 async 函数中才行。


如果我们的异步遍历中出现异常,则可以在 for-await-of 中使用 try catch 来捕获这个异常:


function createRejectingIterable() {return {Symbol.asyncIterator {return this;},next() {return Promise.reject(new Error('Problem!'));},};}(async function () {try {for await (const x of createRejectingIterable()) {console.log(x);}} catch (e) {console.error(e);// Error: Problem!}})();同步的 iterable 返回的是同步的 iterators,next 方法返回的是{value, done}。


如果使用 for-await-of 则会将同步的 iterators 转换成为异步的 iterators。然后返回的值被转换成为了 Promise。


如果同步的 next 本身返回的 value 就是 Promise 对象,则异步的返回值还是同样的 promise。


也就是说会把:Iterable<Promise<T>> 转换成为 AsyncIterable<T> ,如下面的例子所示:


async function main() {const syncIterable = [Promise.resolve('a'),Promise.resolve('b'),];for await (const x of syncIterable) {console.log(x);}}main();


// Output:// a// b 上面的例子将同步的 Promise 转换成异步的 Promise。


async function main() {for await (const x of ['a', 'b']) {console.log(x);}}main();


// Output:// c// d 上面的例子将同步的常量转换成为 Promise。 可以看到两者的结果是一样的。


异步 iterable 的生成回到上面的例子,我们使用 createAsyncIterable(syncIterable)将 syncIterable 转换成了 AsyncIterable。


我们看下这个方法是怎么实现的:


async function* createAsyncIterable(syncIterable) {for (const elem of syncIterable) {yield elem;}}上面的代码中,我们在一个普通的 generator function 前面加上 async,表示的是异步的 generator。


对于普通的 generator 来说,每次调用 next 方法的时候,都会返回一个 object {value,done} ,这个 object 对象是对 yield 值的封装。


对于一个异步的 generator 来说,每次调用 next 方法的时候,都会返回一个包含 object {value,done} 的 promise 对象。这个 object 对象是对 yield 值的封装。


因为返回的是 Promise 对象,所以我们不需要等待异步执行的结果完成,就可以再次调用 next 方法。


我们可以通过一个 Promise.all 来同时执行所有的异步 Promise 操作:


const asyncGenObj = createAsyncIterable(['a', 'b']);const [{value:v1},{value:v2}] = await Promise.all([asyncGenObj.next(), asyncGenObj.next()]);console.log(v1, v2); // a b 在 createAsyncIterable 中,我们是从同步的 Iterable 中创建异步的 Iterable。


接下来我们看下如何从异步的 Iterable 中创建异步的 Iterable。


从上一节我们知道,可以使用 for-await-of 来读取异步 Iterable 的数据,于是我们可以这样用:


async function* prefixLines(asyncIterable) {for await (const line of asyncIterable) {yield '> ' + line;}}在 generator 一文中,我们讲到了在 generator 中调用 generator。也就是在一个生产器中通过使用 yield*来调用另外一个生成器。


同样的,如果是在异步生成器中,我们可以做同样的事情:


async function* gen1() {yield 'a';yield 'b';return 2;}async function* gen2() {const result = yield* gen1();// result === 2}


(async function () {for await (const x of gen2()) {console.log(x);}})();// Output:// a// b 如果在异步生成器中抛出异常,这个异常也会被封装在 Promise 中:


async function* asyncGenerator() {throw new Error('Problem!');}asyncGenerator().next().catch(err => console.log(err)); // Error: Problem!异步方法和异步生成器异步方法是使用 async function 声明的方法,它会返回一个 Promise 对象。


function 中的 return 或 throw 异常会作为返回的 Promise 中的 value。


(async function () {return 'hello';})().then(x => console.log(x)); // hello


(async function () {throw new Error('Problem!');})().catch(x => console.error(x)); // Error: Problem!异步生成器是使用 async function * 申明的方法。它会返回一个异步的 iterable。


通过调用 iterable 的 next 方法,将会返回一个 Promise。异步生成器中 yield 的值会用来填充 Promise 的值。如果在生成器中抛出了异常,同样会被 Promise 捕获到。


async function* gen() {yield 'hello';}const genObj = gen();genObj.next().then(x => console.log(x));// { value: 'hello', done: false }本文作者:flydean 程序那些事


本文链接:http://www.flydean.com/es9-async-iteration/


本文来源:flydean 的博客


欢迎关注我的公众号:「程序那些事」最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

发布于: 2021 年 04 月 15 日阅读数: 13
用户头像

关注公众号:程序那些事,更多精彩等着你! 2020.06.07 加入

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧,尽在公众号:程序那些事!

评论

发布
暂无评论
ES9的新特性:异步遍历Async iteration