写点东西《理解 JavaScript 中的异步迭代器》

发布时间:2024年01月15日

一段时间前,我在 Medium 上发了一篇博文,在其中我讨论了 Iterator 协议及其用户界面。然而,除了 Promise.finally 等 API 之外,ECMAScript 2018 还为我们带来了另一种处理迭代器的方法。异步迭代器。

?问题

我们假设自己处于一种相当常见的情形中。我们正在使用 Node.js,并且必须逐行读取一个文件。Node 有一个用于此类函数的 API,称为 readLine (在此处查看完整文档)。此 API 是一个包装器,因此您可以逐行读取输入流中的数据,而无需解析输入缓冲区并将文本分解成小块。

它公开了一个事件 API,您可以像这样进行监听:

const fs = require('fs')
const readline = require('readline')
const reader = readline.createInterface({
  input: fs.createReadStream('./file.txt'),
  crlfDelay: Infinity
})

reader.on('line', (line) => console.log(line))

想象一下我们有一个简单文件:

line 1
line 2
line 3

如果我们对已创建的文件运行此代码,我们将在控制台上获得逐行输出。但是,使用事件并不是编写可维护代码的最佳方式之一,因为事件是完全异步的,并且它们可能会破坏代码的流程,因为它们会无序触发,并且您只能通过侦听器分配一个操作。

?解决方案

除了事件 API, readline 还公开了一个 async iterator 。这意味着,我们不会通过 line 事件中的侦听器读取行,而是会通过使用 for 关键字的新方式读取行。

今天,我们有几种使用 for 循环的选项。第一个是最常见的模型,使用计数器和条件:

for (let x = 0; x < array.length; x++) {
  // Code here 
}

我们还可以使用符号 for ... in 符号来读取数组索引:

const a = [1,2,3,4,5,6]
for (let index in a) {
  console.log(a[index])
}

在前面的情况下,我们将在 console.log 中获得输出,从 1 到 6 的数字,但是如果我们使用 console.log(index) ,我们将记录数组的索引,即从 0 到 5 的数字。

对于下一个情况,我们可以使用 for ... of 符号直接获取数组的可枚举属性,即其直接值:

const a = [1,2,3,4,5,6]

for (let item of a) {
  console.log(item)
}

请注意,我描述的所有方式都是同步的。那么,我们如何按顺序读取一系列承诺?

想象一下,我们有另一个总是返回 Promise 的接口,该 Promise 会针对我们所讨论的文件行进行解析。为了按顺序解析这些 Promise,我们需要执行类似以下操作:

async function readLine (files) {
  for (const file of files) {
    const line = await readFile(file) // Imagine readFile is our cursor
    console.log(line)
  }
}

但是,由于异步可迭代对象(如 readline )的神奇之处,我们可以执行以下操作:

const fs = require('fs')
const readline = require('readline')
const reader = readline.createInterface({
  input: fs.createReadStream('./xpto.txt'),
  crlfDelay: Infinity
})

async function read () {
  for await (const line of reader) {
    console.log(line)
  }
}

read()

注意我们现在使用 for 的新定义, for await (const x of y)


对于 Await 和 Node.js

Node.js 运行时从版本 10.x 开始原生支持 for await 表示法。如果您使用的是版本 8.x 或 9.x,则需要使用 --harmony_async_iteration 标志启动 Javascript 文件。遗憾的是,Node.js 版本 6 或 7 不支持异步迭代器。

为了理解异步迭代器的概念,我们需要了解迭代器本身是什么。我之前的文章是一份很好的信息来源,但简而言之,迭代器是一个公开 next() 函数的对象,该函数返回另一个带有表示法 {value: any, done: boolean} 的对象,其中 value 是当前迭代的值,而 done 标识序列中是否还有更多值。

一个简单的示例是一个迭代器,它遍历数组中的所有项:

const array = [1,2,3]
let index = 0

const iterator = {
  next: () => {
    if (index >= array.length) return { done: true }
    return {
      value: array[index++],
      done: false
    }
  }
}

单独来看,迭代器没有任何实际用途,因此为了从中获取一些用途,我们需要一个 iterableiterable 是一个具有 Symbol.iterator 键的对象,该键返回一个函数,该函数返回我们的迭代器:

// ... Iterator code here ...

const iterable = {
  [Symbol.iterator]: () => iterator
}

现在我们可以正常使用它,使用 for (const x of iterable) ,我们将使 array 中的所有值逐个迭代。

如果您想进一步了解符号,请查看我专门为此撰写的另一篇文章。

在底层,所有数组和对象都有一个 Symbol.iterator ,以便我们可以执行 for (let x of [1,2,3]) 并返回我们想要的值。

正如您可能期望的那样,异步迭代器与迭代器完全相同,只是我们可以在可迭代对象中使用 Symbol.asyncIterator 而不是 Symbol.iterator ,并且我们有一个返回 {value, done} 的对象而不是解析为具有相同签名的对象的 Promise。

让我们将上面的迭代器转换为异步迭代器:

const array = [1,2,3]
let index = 0

const asyncIterator = {
  next: () => {
  if (index >= array.length) return Promise.resolve({done: true})
  return Promise.resolve({value: array[index++], done: false})
  }
}

const asyncIterable = {
  [Symbol.asyncIterator]: () => asyncIterator
}


异步迭代

我们可以通过调用 next() 函数手动迭代任何迭代器:

// ... Async iterator Code here ...

async function manual () {
  const promise = asyncIterator.next() // Promise
  await p // Object { value: 1, done: false }
  await asyncIterator.next() // Object { value: 2, done: false }
  await asyncIterator.next() // Object { value: 3, done: false }
  await asyncIterator.next() // Object { done: true }
}

为了迭代我们的异步迭代器,我们必须使用 for await ,但请记住关键字 await 只能在 async function 内使用,这意味着我们必须拥有类似这样的东西:

// ... Code above ...

async function iterate () {
  for await (const num of asyncIterable) console.log(num) 
}

iterate() // 1, 2, 3

但是,由于异步迭代器在 Node 8.x 或 9.x 中不受支持(我知道非常老了),因此为了在这些版本中使用异步迭代器,我们可以简单地从对象中提取 next 并手动对其进行迭代:

// ... Async Iterator Code here ...

async function iterate () {
  const {next} = asyncIterable[Symbol.asyncIterator]() // we take the next iterator function

  for (let {value, done} = await next(); !done; {value, done} = await next()) {
    console.log(value)
  }
}

请注意, for await 更加简洁,也更加干净,因为它表现得像一个常规循环,但除此之外,它还比理解起来简单得多,它通过 done 键自行检查迭代器的结尾。

?处理错误

如果我们的承诺在迭代器中被拒绝,会发生什么?好吧,就像任何被拒绝的承诺一样,我们可以通过一个简单的 try/catch 来捕获它的错误(因为我们正在使用 await ):

const asyncIterator = { next: () => Promise.reject('Error') }
const asyncIterable = { [Symbol.asyncIterator]: () => asyncIterator }

async function iterate () {
  try {
    for await (const num of asyncIterable) {}
  } catch (e) {
    console.log(e.message)
  }
}

iterate()

?回退

关于异步迭代器非常有趣的一点是,它们有一个 Symbol.iterator 的后备,这意味着您也可以将其与常规迭代器一起使用,例如一个 Promise 数组:

const promiseArray = [
  fetch('https://lsantos.dev'),
  fetch('https://lsantos.me')
]

async function iterate () {
  for await (const response of promiseArray) console.log(response.status)
}

iterate() // 200, 200

?异步生成器

在大多数情况下,可以从生成器创建迭代器和异步迭代器。

生成器是允许暂停和恢复其执行的函数,因此可以执行执行,然后通过 next() 函数获取下一个值。

这是一个非常简单的生成器描述,有必要阅读仅讨论它们的相关文章,以便您可以快速深入地理解生成器。

异步生成器表现得像一个异步迭代器,但您必须手动实现停止机制,例如,让我们构建一个用于 git 提交的随机消息生成器,让您的同事对他们的贡献超级满意:

async function* gitCommitMessageGenerator () {
  const url = 'https://whatthecommit.com/index.txt'

  while (true) {
    const response = await fetch(url)
    yield await response.text() // We return the value
  }
}

请注意,我们从未返回过 {value, done} 对象,因此循环无法知道何时执行已完成。我们可以实现这样的函数:

// Previous Code
async function getCommitMessages (times) {
  let execution = 1
  for await (const message of gitCommitMessageGenerator()) {
    console.log(message)
    if (execution++ >= times) break
  }
}

getCommitMessages(5)
// I'll explain this when I'm sober .. or revert it
// Never before had a small typo like this one caused so much damage.
// For real, this time.
// Too lazy to write descriptive message
// Ugh. Bad rebase.

?用例

对于一个更有趣的示例,让我们为一个实际用例构建一个异步迭代器。目前,Oracle Database 驱动程序支持 Node.js resultSet API,该 API 在数据库上执行查询并返回一个记录流,可以使用 getRow()method 一个一个地读取这些记录。

要创建此 resultSet ,我们需要在数据库中执行一个查询,如下所示:

const oracle = require('oracledb')
const options = {
  user: 'example',
  password: 'example123',
  connectString: 'string'
}

async function start () {
  const connection = await oracle.getConnection(options)
  const { resultSet } = await connection.execute('query', [], { outFormat: oracle.OBJECT, resultSet: true })
  return resultSet
}

start().then(console.log)

我们的 resultSet 有一个名为 getRow() 的方法,它返回一个 Promise,其中包含要从数据库中获取的下一行。这是一个异步迭代器的不错用例,不是吗?我们可以创建一个逐行返回此 resultSet 的游标。让我们通过创建一个 Cursor 类使其变得更复杂一些:

class Cursor {
  constructor(resultSet) {
    this.resultSet = resultSet
  }

  getIterable() {
    return {
      [Symbol.asyncIterator]: () => this._buildIterator()
    }
  }

  _buildIterator() {
    return {
      next: () => this.resultSet.getRow().then((row) => ({ value: row, done: row === undefined }))
    }
  }
}

module.exports = Cursor

查看光标接收它应该处理的 resultSet ,并将其存储在其当前状态中。因此,让我们更改以前的方法,以便一次返回光标而不是 resultSet

const oracle = require('oracledb')
const options = {
  user: 'example',
  password: 'example123',
  connectString: 'string'
}
async function getResultSet() {
  const connection = await oracle.getConnection(options)
  const { resultSet } = await connection.execute('query', [], { outFormat: oracle.OBJECT, resultSet: true })
  return resultSet
}

async function start() {
  const resultSet = await getResultSet()
  const cursor = new Cursor(resultSet)

  for await (const row of cursor.getIterable()) {
    console.log(row)
  }
}

start()

这样我们就可以循环遍历所有返回的行,而无需单独的 Promises 解析。

?结论

异步迭代器非常强大,尤其是在像 JavaScript 这样的动态和异步语言中。使用它们,您可以将复杂的执行变成简单的代码,从而对用户隐藏大部分复杂性。

文章来源:https://blog.csdn.net/l316278750/article/details/135602346
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。