/ ECMAScript6

ECMAScript6 之 异步 Generator 函数

标签: ECMAScript 6 javaScript

简介

就像 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: true } 的对象,而异步 Generator 函数返回一个 Promise 对象,将异步操作的结果返回在 then 方法中,结果即为 { value: any, done: true }

为了方便称呼,我们将异步 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 asyncIterator.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 中是不存在正真的异步同步化,这点需要牢记。

注: 上面的例子大多数来自《ECMAScript 6入门》这本书。