ES9 异步 Generator

分享

简介

就像 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

如果还记得同步遍历器的编写方式的话,相信看到这些代码会很眼熟,区别仅仅只有两点

  1. 接口名称为:Symbol.asyncIterator
  2. 使用 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

asyncIteratorasync Generator 的关系和 IteratorGenerator 的关系是一致的,前者规定了一个接口的形式,后者为前者产出规定形式的接口。而在异步操作中,我们一般不会自己去写符合 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 。不仅代码简洁,而且执行顺序也是我们想要的。

总结

asyncIteratorasync Generator 的配合使用使我们能更好的去控制有顺序的且结构一致的异步循环,但我们始终要明确一点:这一切都是基于 Promise 实现了,不论流程上多么的清晰明了,这和 async/await 一样,都仅仅是一块语法糖,javascript 中是不存在正真的异步同步化,这点需要牢记。

阅读更多

JavaScript 对象 & 原型

前言 这次的 why what or how 主题:JavaScript 对象 & 原型。 此类文章在百度上一搜一大把,其实不用再写了,但是本着把这个问题解释的清清楚楚明明白白,还是开始写了。 原因如下: 1. 面试宝典类文章,弄张图片一糊弄,让人觉得自己理解了。 2. 解释类文章,告诉你一堆语法,对语法一顿解释,告诉你就是这样的,还是没说清楚。 3. 很少有文章单独解释这个点!但这个点是基础!真的很重要! 所以本篇文章想说一说对象 & 原型,但为了确保能顺利理解,请先看完 JS 变量存储?栈 & 堆?NONONO!,因为该篇文章从变量存储的角度来解释 JavaScript 对象 & 原型,请确保看完,在看这篇文章。 什么是对象? 既然要说清楚,

By Breeze

浏览器下的 Event Loop

前言 javascript 是以单线程的形式运行在宿主环境下,javascript 采用了回调的形式来解决异步任务。 为什么是单线程? javascript 的最开始的出现是为了给 web 页面增添一些动态的效果,那么就避免不了获取页面上的元素信息,如果 javascript 是以多线程的形式运行在浏览器内,如果两个线程内的 javascript 同时去获取/修改,某个页面上的元素,那么浏览器该让哪个 javascript 线程拥有获取/修改该元素的权限呢?由于元素的信息会经常性的发生变化,那么又改如何去同步各个线程内所保存的元素信息呢? 所以综合以上问题, javascript 是单线程的原因就显示意见了,单线程在执行时,对于元素信息的引用在同一时间仅可能只有一个,那么以上所有的问题都不存在了。 什么是异步任务? 任何代码在执行时,都会碰到一些需要经过大量时间运算或是等待的代码,在浏览器的环境下,常见的就是 http 任务,比如:资源的加载(图片的 onload 事件),ajax 的请求(XMLHttpRequest 的 onLoad 事件)还有页面元素的点击事件以及

By Breeze

node 下的 Event Loop

前言 通过 浏览器下的 Event Loop ,可以得知 javascript 是一门事件驱动的语言, javascript 主线程通过不断的调用事件队列中的事件来完成异步任务。那么 Node 下是否也是如此? Node 下的事件队列 在 Node 官网有这样一篇文章:The Node.js Event Loop, Timers, and process.nextTick() 。 该文章主要讲述了 Node 中是如何处理以及实现 Node 下的 Event Loop。 主要包含以下内容: 事件队列 不同于浏览器,Node 下的有 6 个事件处理阶段,其执行顺序和队列名称如下: ┌───────────────────────────┐ ┌─>│ timers │ │ └─────────────┬─────────────┘ │ ┌─────────────┴─────────────┐ │ │ pending callbacks │ │ └─────────────┬─────────────┘ │

By Breeze

什么是 HTML 5?

前言 作为程序员,技术的落实与巩固是必要的,因此想到写个系列,名为 why what or how 每篇文章试图解释清楚一个问题。 这次的 why what or how 主题:现在几乎所有人都知道了 HTML5 ,那么 H5 到底相比于 HTML4 有什么区别呢? 升级版?标准版! HTML5 做为 HTML 标准的第 5 版,于 2014 年发布,相信关注过 HTML5 的发展史的朋友都知道该版本是 WHATWG 和 W3C 握手言和后诞生的,是 W3C 组织与浏览器厂商相互妥协的结果,其中的绝大部分规范都由 WHATWG 组织所制定,之后由 W3C 采纳并入标准中。

By Breeze