Skip to content

异步遍历器

同步遍历器的问题

Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的 next 方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。next 方法返回的对象的结构是{value, done},其中 value 表示当前的数据的值,done 是一个布尔值,表示遍历是否结束。

js
function idMaker() {
  let index = 0

  return {
    next: function () {
      return { value: index++, done: false }
    }
  }
}

const it = idMaker()

it.next().value // 0
it.next().value // 1
it.next().value // 2
// ...

隐含着一个规定,it.next()方法必须是同步的,只要调用就必须立刻返回值。不适合异步操作。

解决方法:将异步操作包装成 Thunk 函数或者 Promise 对象,即 next()方法返回值的 value 属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而 done 属性则还是同步产生的

异步操作包装为 Promise

js
function idMaker() {
  let index = 0

  return {
    next: function () {
      return {
        value: new Promise((resolve) => setTimeout(() => resolve(index++), 1000)),
        done: false
      }
    }
  }
}

const it = idMaker()

it.next().value.then((o) => console.log(o)) // 0
it.next().value.then((o) => console.log(o)) // 1
it.next().value.then((o) => console.log(o)) // 2
// ...

问题:语义不清晰,迭代器返回 Promise,还要进行 .then 操作

ES2018 引入了“异步遍历器”(Async Iterator),为异步操作提供原生的遍历器接口,即 value 和 done 这两个属性都是异步产生

异步遍历的接口

异步遍历器特点:调用遍历器的 next 方法,返回的是一个 Promise 对象。

异步遍历器接口,部署在 Symbol.asyncIterator 属性。Symbol.asyncIterator 属性有值,表示可以进行异步遍历

异步遍历器的例子

js
const asyncIterable = createAsyncIterable(['a', 'b'])
const asyncIterator = asyncIterable[Symbol.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 }
  })

异步遍历器返回了两次值

  • 第一次,返回 Promise
  • 第二次,等待 Promise resolve,再返回

异步遍历器的 next 方法是可以连续调用的。

用法 1:所有的 next 方法放在 Promise.all 方法里面

js
const asyncIterable = createAsyncIterable(['a', 'b'])
const asyncIterator = asyncIterable[Symbol.asyncIterator]()
const [{ value: v1 }, { value: v2 }] = await Promise.all([asyncIterator.next(), asyncIterator.next()])

console.log(v1, v2) // a b

用法 2:一次性调用所有的 next 方法,然后 await 最后一步操作

js
async function runner() {
  const writer = openFile('someFile.txt')
  writer.next('hello')
  writer.next('world')
  await writer.return()
}

runner()

for await...of

新引入的 for await...of 循环,用于遍历异步的 Iterator 接口。

js
async function f() {
  for await (const x of createAsyncIterable(['a', 'b'])) {
    console.log(x)
  }
}
// a
// b

for...of 循环自动调用这个对象的异步遍历器的 next 方法,会得到一个 Promise 对象。await 用来处理这个 Promise 对象,一旦 resolve,就把得到的值(x)传入 for...of 的循环体

next 方法返回的 Promise 对象被 reject,for await...of 就会报错,要用 try...catch 捕捉

js
async function () {
  try {
    for await (const x of createRejectingIterable()) {
      console.log(x);
    }
  } catch (e) {
    console.error(e);
  }
}

for await...of 循环也可以用于同步遍历器

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

Node v10 支持异步遍历器,Stream 就部署了这个接口

js
// 传统写法
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 ###')
}

异步 Generator 函数

Generator 函数返回一个同步遍历器对象,异步 Generator 函数返回一个异步遍历器对象。

语法上:异步 Generator 函数就是async函数与 Generator 函数的结合

js
async function* gen() {
  yield 'hello'
}
const genObj = gen()
genObj.next().then((x) => console.log(x))
// { value: 'hello', done: false }

异步遍历器设计目之一:Generator 函数 用 一套接口处理同步和异步

js
// 同步 Generator 函数
function* map(iterable, func) {
  const iter = iterable[Symbol.iterator]()
  while (true) {
    const { value, done } = iter.next()
    if (done) break
    yield func(value)
  }
}

// 异步 Generator 函数
async function* map(iterable, func) {
  const iter = iterable[Symbol.asyncIterator]()
  while (true) {
    const { value, done } = await iter.next()
    if (done) break
    yield func(value)
  }
}

另一个异步 Generator 函数的例子

js
async function* readLines(path) {
  let file = await fileOpen(path)

  try {
    while (!file.EOF) {
      yield await file.readLine()
    }
  } finally {
    await file.close()
  }
}

await:返回 Promise 对象,将外部操作产生值输入函数内部。

yield:暂停 next 方法,将函数内布值输出。

异步 Generator 函数用法

js
;(async function () {
  for await (const line of readLines(filePath)) {
    console.log(line)
  }
})()

例子:

js
function fetchRandom() {
  const url = 'https://www.random.org/decimal-fractions/' + '?num=1&dec=10&col=1&format=plain&rnd=new'
  return fetch(url)
}

async function* asyncGenerator() {
  console.log('Start')
  const result = await fetchRandom() // (A)
  yield 'Result: ' + (await result.text()) // (B)
  console.log('Done')
}

const ag = asyncGenerator()
ag.next().then(({ value, done }) => {
  console.log(value)
})

yield命令后面是一个字符串,会被自动包装成一个 Promise 对象。

代码执行顺序

  1. ag.next()立刻返回一个 Promise 对象。
  2. asyncGenerator函数开始执行,打印出Start
  3. await命令返回一个 Promise 对象,asyncGenerator函数停在这里。
  4. A 处变成 fulfilled 状态,产生的值放入result变量,asyncGenerator函数继续往下执行。
  5. 函数在 B 处的yield暂停执行,一旦yield命令取到值,ag.next()返回的那个 Promise 对象变成 fulfilled 状态。
  6. ag.next()后面的then方法指定的回调函数开始执行。该回调函数的参数是一个对象{value, done},其中value的值是yield命令后面的那个表达式的值,done的值是false

异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为 reject,然后抛出的错误被 catch 方法捕获

js
async function* asyncGenerator() {
  throw new Error('Problem!')
}

asyncGenerator()
  .next()
  .catch((err) => console.log(err)) // Error: Problem!

普通 async 函数返回 Promise 对象,异步 Generator 函数返回异步 Iterator 对象。async 函数 和 异步 Generator 函数,是封装异步操作的两种方法。前者自带执行器,后者通过 for await ... of 执行 或 自定义执行器

js
async function takeAsync(asyncIterable, count = Infinity) {
  const result = []
  const iterator = asyncIterable[Symbol.asyncIterator]()
  while (result.length < count) {
    const { value, done } = await iterator.next()
    if (done) break
    result.push(value)
  }
  return result
}

自动执行器使用实例

js
async function f() {
  async function* gen() {
    yield 'a'
    yield 'b'
    yield 'c'
  }

  return await takeAsync(gen())
}

f().then(function (result) {
  console.log(result) // ['a', 'b', 'c']
})

异步 Generator 函数以后,JavaScript 就有四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数

作用区分

async 函数:一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘)

异步 Generator:一系列产生相同数据结构的异步操作(比如一行一行读取文件)

异步 Generator 函数可以通过 next 方法参数,接收外部传入的数据

js
const writer = openFile('someFile.txt')
writer.next('hello') // 立即执行
writer.next('world') // 立即执行
await writer.return() // 等待写入结束

代码分析:通过 next 向函数内部传参,每次 next 同步执行,通过 await 等待整个写入操作。

同步的数据结构,也可以使用异步 Generator 函数

js
async function* createAsyncIterable(syncIterable) {
  for (const elem of syncIterable) {
    yield elem
  }
}

yield* 语句

yield*语句处理异步遍历器

js
async function* gen1() {
  yield 'a'
  yield 'b'
  return 2
}

async function* gen2() {
  const result = yield* gen1()
  console.log('result ', result)
}

与同步 Generator 函数一样,for await...of循环会展开yield*

js
;(async function () {
  for await (const x of gen2()) {
    console.log(x)
  }
})()
// result 2
// a
// b

相关链接

[-] 异步遍历器