迭代器

迭代器模式是一种基本模式,它非常重要且常用,因此通常内置于编程语言本身中。 所有主要编程语言都以一种或另一种方式实现该模式,当然包括 JavaScript(从 ECMAScript2015 规范开始)。

迭代器模式定义了用于迭代容器元素(例如数组或树数据结构)的通用接口或协议。 通常,根据数据的实际结构,迭代容器元素的算法会有所不同。 考虑一下迭代数组与遍历树:在第一种情况下,我们只需要一个简单的循环;在第一种情况下,我们只需要一个简单的循环;在第一种情况下,我们只需要一个简单的循环。 第二个需要更复杂的树遍历算法(nodejsdp.link/tree-traversal)。 通过迭代器模式,我们隐藏了有关所使用的算法或数据结构的详细信息,并提供了一个用于迭代任何类型容器的通用接口。 本质上,迭代器模式允许我们将遍历算法的实现与我们使用遍历操作的结果(元素)的方式解耦。

然而,在 JavaScript 中,即使与其他类型的构造(不一定是容器)(例如事件发射器和流)一起使用,迭代器也能很好地工作。 因此,我们可以用更一般的术语来说,迭代器模式定义了一个接口来迭代按顺序生成或检索的元素。

iterator 协议

在 JavaScript 中,迭代器模式是通过协议而不是通过正式构造(例如继承)来实现的。 这本质上意味着迭代器模式的实现者和使用者之间的交互将使用预先约定形状的接口和对象进行通信。 在 JavaScript 中实现迭代器模式的起点是迭代器协议,它定义了用于生成值序列的接口。 因此,我们将迭代器称为实现 next() 方法的对象,该方法具有以下行为:每次调用该方法时,该函数都会通过一个对象返回迭代中的下一个元素,称为迭代器结果,具有两个属性 - done 和 value:

  • 当迭代完成时,或者换句话说,当没有更多元素可返回时,done 将设置为 true。 否则,done 将是 undefined 或 false。

  • value 包含迭代的当前元素,如果 done 为 true,则可以保留未定义。 如果即使在 did 为 true 时也设置了 value,则表示 value 包含迭代的返回值,该值不是被迭代的元素的一部分,但它与整个迭代本身相关(例如, 迭代所有元素所花费的时间或迭代的所有元素的平均值(如果元素是数字)。

没有什么可以阻止我们向迭代器返回的对象添加额外的属性。 然而,这些属性将被使用迭代器的内置构造或 API 简单地忽略(我们稍后会遇到这些属性)。

让我们用一个简单的例子来演示如何实现迭代器协议。 让我们实现一个名为 createAlphabetIterator() 的工厂函数,它创建一个迭代器,允许我们遍历英文字母表中的所有字母。 这样的函数看起来像这样:

const A_CHAR_CODE = 65
const Z_CHAR_CODE = 90

function createAlphabetIterator () {
    let currCode = A_CHAR_CODE

    return {
        next () {
            const currChar = String.fromCodePoint(currCode)
            if (currCode > Z_CHAR_CODE) {
                return { done: true }
            }

            currCode++
            return { value: currChar, done: false }
        }
    }
}

迭代的逻辑实际上非常简单; 在每次调用 next() 方法时,我们只需增加一个表示字母字符代码的数字,将其转换为字符,然后使用迭代器协议定义的对象形状返回它。

迭代器并不需要返回 done: true。 事实上,在很多情况下迭代器都是无限的。 一个例子是迭代器在每次迭代时返回一个随机数。 另一个例子是计算数学级数的迭代器,例如斐波那契级数或常数 pi 的数字(作为练习,您可以尝试将以下算法转换为使用迭代器:nodejsdp.link/pi-js)。 请注意,即使迭代器理论上是无限的,也不意味着它没有计算或空间限制。 例如,斐波那契数列返回的数字很快就会变得非常大。

需要注意的重要一点是,迭代器通常是有状态对象,因为我们必须以某种方式跟踪迭代的当前位置。 在前面的示例中,我们设法将状态保持在闭包中(currCode 变量),但这只是我们可以这样做的方法之一。 例如,我们可以将状态保存在实例变量中。 这在可调试性方面通常更好,因为我们可以随时从迭代器本身读取迭代的状态,但另一方面,它并不能阻止外部代码修改实例变量,从而篡改迭代器的状态 迭代。 由您决定每个选项的利弊。

迭代器确实也可以是完全无状态的。 示例包括返回随机元素的迭代器,并且随机完成或从未完成,以及迭代器在第一次迭代时停止。

现在,让我们看看如何使用刚刚构建的迭代器。 考虑以下代码片段:

const iterator = createAlphabetIterator()

let iterationResult = iterator.next()
while (!iterationResult.done) {
    console.log(iterationResult.value)
    iterationResult = iterator.next()
}

从前面的代码中我们可以看到,使用迭代器的代码本身可以被视为一种模式。 然而,正如我们将在本节后面看到的,这并不是我们使用迭代器的唯一方法。 事实上,JavaScript 有更方便、更优雅的方式来使用迭代器。

迭代器可以选择指定两个附加方法:return([value]) 和 throw(error)。 第一个按照约定用于向迭代器发出信号,告知使用者在完成之前已停止迭代,而第二个用于向迭代器传达已发生错误情况。 内置迭代器很少使用这两种方法。

iterable 协议

可迭代协议定义了对象返回迭代器的标准化方法。 这样的对象称为可迭代对象。 通常,可迭代对象是元素的容器,例如数据结构,但它也可以是虚拟表示一组元素的对象,例如 Directory 对象,它允许我们迭代目录中的文件。

在 JavaScript 中,我们可以通过确保它实现 @@iterator 方法来定义可迭代对象,或者换句话说,可以通过内置符号 Symbol.iterator 访问的方法。

@@name 约定表示根据 ES6 规范的众所周知的符号。 要了解更多信息,您可以在 nodejsdp.link/es6-wellknown-symbols 上查看 ES6 规范的相关部分。

这样的 @@iterator 方法应该返回一个迭代器对象,该对象可用于迭代由 iterable 表示的元素。 例如,如果我们的可迭代对象是一个类,那么我们将具有如下所示的内容:

class MyIterable {
    // other methods...
    [Symbol.iterator] () {
        // return an iterator
    }
}

为了展示其在实践中的工作原理,让我们构建一个类来管理以二维矩阵结构组织的信息。 我们希望此类实现可迭代协议,以便我们可以使用迭代器扫描矩阵中的所有元素。 让我们创建一个名为 Matrix.js 的文件,其中包含以下内容:

export class Matrix {
    constructor (inMatrix) {
        this.data = inMatrix
    }

    get (row, column) {
        if (row >= this.data.length ||
            column >= this.data[row].length) {
            throw new RangeError('Out of bounds')
        }
        return this.data[row][column]
    }

    set (row, column, value) {
        if (row >= this.data.length ||
            column >= this.data[row].length) {
            throw new RangeError('Out of bounds')
        }
        this.data[row][column] = value
    }

    [Symbol.iterator] () {
        let nextRow = 0
        let nextCol = 0

        return {
            next: () => {
                if (nextRow === this.data.length) {
                    return { done: true }
                }

                const currVal = this.data[nextRow][nextCol]

                if (nextCol === this.data[nextRow].length - 1) {
                    nextRow++
                    nextCol = 0
                } else {
                    nextCol++
                }

                return { value: currVal }
            }
        }
    }
}

正如我们所看到的,该类包含用于获取和设置矩阵中的值的基本方法,以及实现我们的可迭代协议的 @@iterator 方法。 @@iterator 方法将返回一个由可迭代协议指定的迭代器,并且这样的迭代器遵守迭代器协议。 迭代器的逻辑非常简单:我们只是通过扫描每行的每一列,从左上角到右下角遍历矩阵的单元格; 我们通过利用两个索引 nextRow 和 nextCol 来做到这一点。

现在,是时候尝试我们的可迭代 Matrix 类了。 我们可以在一个名为index.js的文件中做到这一点:

import { Matrix } from './matrix.js'

const matrix2x2 = new Matrix([
    ['11', '12'],
    ['21', '22']
])

const iterator = matrix2x2[Symbol.iterator]()
let iterationResult = iterator.next()
while (!iterationResult.done) {
    console.log(iterationResult.value)
    iterationResult = iterator.next()
}

我们在前面的代码中所做的就是创建一个示例 Matrix 实例,然后使用 @@iterator 方法获取迭代器。 正如我们所知,接下来发生的只是迭代器返回的元素的样板代码。 迭代的输出应该是 “11”、“12”、“21”、“22”。

作为原生 JavaScript 接口的迭代器和可迭代对象

此时,您可能会问:“使用所有这些协议来定义迭代器和可迭代对象有什么意义?” 好吧,拥有标准化接口允许第三方代码以及语言本身围绕我们刚刚看到的两个协议进行建模。 这样,我们就可以拥有 API(甚至是原生的)以及接受迭代作为输入的语法结构。

例如,接受可迭代的最明显的语法结构是 for…​of 循环。 我们刚刚在上一个代码示例中看到,迭代 JavaScript 迭代器是一个非常标准的操作,并且其代码大部分是样板文件。 事实上,我们总是会调用 next() 来检索下一个元素,并检查以验证迭代结果的 did 属性是否设置为 true,这表示迭代结束。 但是,不用担心,只需将一个可迭代对象传递给 for…​of 指令即可无缝循环其迭代器返回的元素。 这使我们能够使用直观且紧凑的语法来处理迭代:

for (const element of matrix2x2) {
    console.log(element)
}

与可迭代兼容的另一个构造是扩展运算符:

const flattenedMatrix = [...matrix2x2]
console.log(flattenedMatrix)

类似地,我们可以将 iterable 与解构赋值操作一起使用:

const [oneOne, oneTwo, twoOne, twoTwo] = matrix2x2
console.log(oneOne, oneTwo, twoOne, twoTwo)

以下是一些接受可迭代的 JavaScript 内置 API:

  • Map([iterable]): nodejsdp.link/map-constructor

  • WeakMap([iterable]): nodejsdp.link/weakmap-constructor

  • Set([iterable]): nodejsdp.link/set-constructor

  • WeakSet([iterable]): nodejsdp.link/weakset-constructor

  • Promise.all(iterable): nodejsdp.link/promise-all

  • Promise.race(iterable): nodejsdp.link/promise-race

  • Array.from(iterable): nodejsdp.link/array-from

在 Node.js 方面,一个值得注意的接受可迭代的 API 是stream.Readable。 from(iterable, [options]) (nodejsdp.link/read-from),它从可迭代对象中创建一个可读流。

请注意,我们刚刚看到的所有 API 和语法结构都接受可迭代对象而不是迭代器作为输入。 但是,如果我们有一个返回迭代器的函数(例如在我们的 createAlphabetIterator() 示例中),我们该怎么办? 我们如何利用所有内置 API 和语法结构? 一个可能的解决方案是在迭代器对象本身中实现 @@iterator 方法,该方法将简单地返回迭代器对象本身。 这样我们就可以编写如下内容:

for (const letter of createAlphabetIterator()) {
    //...
}

JavaScript 本身定义了许多可迭代对象,可以与我们刚刚看到的 API 和构造一起使用。 最值得注意的 iterable 是 Array,但其他数据结构,例如 Map 和 Set,甚至 String 都实现了 @@iterable 方法。 在 Node.js 领域,Buffer 可能是最著名的可迭代对象。

确保数组不包含重复元素的技巧如下:const uniqArray = Array.from(new Set(arrayWithDuplicates))。 这也向我们展示了迭代器如何为不同组件提供一种使用共享接口相互通信的方法。

生成器

ES2015 规范引入了与迭代器密切相关的语法构造。 我们谈论的是生成器,也称为半协程。 它们是标准函数的概括,其中可以有不同的入口点。 在标准函数中,我们只能有一个入口点,它对应于函数本身的调用,但生成器可以暂停(使用 yield 语句),然后在稍后恢复。 除此之外,生成器非常适合实现迭代器,事实上,正如我们稍后将看到的,生成器函数返回的生成器对象确实既是迭代器又是可迭代的。

理论上的生成器

要定义生成器函数,我们需要使用 function* 声明(function 关键字后跟星号):

function * myGenerator () {
    // generator body
}

调用生成器函数不会立即执行其主体。 相反,它将返回一个生成器对象,正如我们已经提到的,它既是一个迭代器又是一个可迭代对象。 但事情并没有就此结束; 在生成器对象上调用 next() 将启动或恢复生成器的执行,直到调用 Yield 指令或生成器返回(隐式或显式使用返回指令)。 在生成器中,使用关键字 yield 后跟值 x 相当于从迭代器返回 {done: false, value: x},而返回值 x 相当于返回 {done: true, value: x}。

一个简单的生成器函数

为了演示我们刚刚学到的内容,让我们看一个名为 FruitGenerator() 的简单生成器,它将生成两个水果名称并返回它们的成熟季节:

function * fruitGenerator () {
    yield 'peach'
    yield 'watermelon'
    return 'summer'
}

const fruitGeneratorObj = fruitGenerator()
console.log(fruitGeneratorObj.next()) // (1)
console.log(fruitGeneratorObj.next()) // (2)
console.log(fruitGeneratorObj.next()) // (3)

前面的代码将打印以下文本:

{ value: 'peach', done: false }
{ value: 'watermelon', done: false }
{ value: 'summer', done: true }

这是对所发生事件的简短解释:

  1. 第一次调用 fruitGeneratorObj.next() 时,生成器开始执行,直到到达第一个 yield 命令,该命令使生成器暂停并将值 “peach” 返回给 呼叫者。

  2. 第二次调用 fruitGeneratorObj.next() 时,生成器从第二个 yield 命令开始恢复,这又使执行再次暂停,同时将值 “watermelon” 返回给调用者。

  3. 最后一次调用 fruitGeneratorObj.next() 导致生成器的执行从最后一条指令(一条 return 语句)恢复,该语句终止生成器,返回值 “summer”,并将结果中的 done 属性设置为 true 目的。

由于生成器对象也是可迭代的,因此我们可以在 for…​of 循环中使用它。 例如:

for (const fruit of fruitGenerator()) {
    console.log(fruit)
}

前面的循环将打印:

peach
watermelon

为什么夏天不印出来? 好吧,summer 不是由我们的生成器生成的,而是被返回的,这表明迭代已完成,summer 作为返回值(而不是作为元素)。

控制生成器迭代器

生成器对象不仅仅是标准迭代器,事实上,它们的 next() 方法可以选择接受一个参数(而根据迭代器协议的规定,它不需要接受一个参数)。 这样的参数作为 yield 指令的返回值传递。 为了展示这一点,让我们创建一个新的简单生成器:

function * twoWayGenerator () {
    const what = yield null
    yield 'Hello ' + what
}

const twoWay = twoWayGenerator()
twoWay.next()
console.log(twoWay.next('world'))

执行时,前面的代码会打印 Hello world。 这意味着发生了以下情况:

  1. 第一次调用 next() 方法时,生成器到达第一个 yield 语句,然后暂停。

  2. 当调用 next('world') 时,生成器从暂停点(位于 yield 指令上)恢复,但这次我们有一个值传递回生成器。 然后该值将被设置为 what 变量。 然后,生成器将 What 变量附加到字符串 “Hello” 并生成结果。

生成器对象提供的另外两个额外功能是可选的 throw() 和 return() 迭代器方法。 第一个行为类似于 next(),但它也会在生成器中抛出异常,就像在最后一个yield 处抛出异常一样,并返回具有 did 和 value 属性的规范迭代器对象。 第二种是 return() 方法,强制生成器终止并返回一个对象,如下所示: {done: true, value: returnArgument} 其中 returnArgument 是传递给 return() 方法的参数。

下面的代码演示了这两种方法:

function * twoWayGenerator () {
    try {
        const what = yield null
        yield 'Hello ' + what
    } catch (err) {
        yield 'Hello error: ' + err.message
    }
}

console.log('Using throw():')
const twoWayException = twoWayGenerator()
twoWayException.next()
console.log(twoWayException.throw(new Error('Boom!')))

console.log('Using return():')
const twoWayReturn = twoWayGenerator()
console.log(twoWayReturn.return('myReturnValue'))

运行前面的代码会将以下内容打印到控制台:

Using throw():
{ value: 'Hello error: Boom!', done: false }
Using return():
{ value: 'myReturnValue', done: true }

正如我们所看到的,一旦第一个 yield 指令返回,twoWayGenerator() 函数就会收到异常。 这就像从生成器内部抛出异常一样,这意味着可以使用 try…​catch 块像任何其他异常一样捕获和处理它。 相反,return() 方法将简单地停止生成器的执行,从而导致生成器将给定值作为返回值提供。

如何使用生成器代替迭代器

生成器对象也是迭代器。 这意味着生成器函数可以用来实现可迭代对象的 @@iterator方法。 为了演示这一点,让我们将之前的矩阵迭代示例转换为生成器。 让我们更新我们的 matrix.js 文件,如下所示:

export class Matrix {
    // ...rest of the methods (stay unchanged)
    * [Symbol.iterator] () { // (1)
        let nextRow = 0 // (2)
        let nextCol = 0

        while (nextRow !== this.data.length) { // (3)
            yield this.data[nextRow][nextCol]

            if (nextCol === this.data[nextRow].length - 1) {
                nextRow++
                nextCol = 0
            } else {
                nextCol++
            }
        }
    }
}

我们刚刚看到的代码片段中有一些有趣的方面。 让我们更详细地分析它们:

  1. 首先要注意的是,@@iterator 方法现在是一个生成器(注意方法名称之前的星号 *)。

  2. 我们用来维护迭代状态的变量现在只是生成器的局部变量,而在 Matrix 类的先前版本中,这两个变量是闭包的一部分。 这是可能的,因为当调用生成器时,其本地状态在重新进入之间被保留。

  3. 我们使用标准循环来迭代矩阵的元素。 这当然比尝试想象一个调用迭代器的 next() 方法的循环更直观。

正如我们所看到的,生成器是从头开始编写迭代器的绝佳替代方案。 它们将提高迭代例程的可读性,并提供相同级别的功能(甚至更好)。

生成器委托指令,yield * iterable,是接受可迭代作为参数的 JavaScript 内置语法的另一个示例。 该指令将循环遍历可迭代的元素并一一生成每个元素。

异步迭代器

到目前为止,我们看到的迭代器从其 next() 方法同步返回一个值。 然而,在 JavaScript 中,尤其是在 Node.js 中,对需要生成异步操作的项进行迭代是很常见的。

例如,想象一下,迭代 HTTP 服务器收到的请求、SQL 查询的结果或分页 REST API 的元素。 在所有这些情况下,能够从迭代器的 next() 方法返回 Promise 会很方便,或者更好的是使用 async/await 构造。

嗯,这正是异步迭代器的本质; 它们是返回 Promise 的迭代器,由于这是唯一的额外要求,这意味着我们还可以使用异步函数来定义迭代器的 next() 方法。 类似地,异步迭代器是实现 @@asyncIterator 方法的对象,或者换句话说,是通过 Symbol.asyncIterator 键访问的方法,该方法(同步)返回异步迭代器。

异步可迭代对象可以使用 for wait…​of 语法进行循环,该语法只能在异步函数内部使用。 使用 for wait…​of 语法,我们本质上是在迭代器模式之上实现顺序异步执行流。 本质上,它只是以下循环的语法糖:

const asyncIterator = iterable[Symbol.asyncIterator]()
let iterationResult = await asyncIterator.next()
while (!iterationResult.done) {
    console.log(iterationResult.value)
    iterationResult = await asyncIterator.next()
}

这意味着 for wait…​of 语法也可用于迭代简单的可迭代对象(不仅仅是异步可迭代对象),例如,迭代 Promise 数组。 即使迭代器的所有元素(或没有)都是 Promise,它也会起作用。

为了快速演示这一点,让我们构建一个类,该类将 URL 列表作为输入,并允许我们迭代它们的可用性状态(向上/向下)。 我们将该类称为 CheckUrls:

import superagent from 'superagent'
export class CheckUrls {
    constructor (urls) { // (1)
        this.urls = urls
    }
    [Symbol.asyncIterator] () {
        const urlsIterator = this.urls[Symbol.iterator]() // (2)
        return {
            async next () { // (3)
                const iteratorResult = urlsIterator.next() // (4)
                if (iteratorResult.done) {
                    return { done: true }
                }
                const url = iteratorResult.value
                try {
                    const checkResult = await superagent // (5)
                        .head(url)
                        .redirects(2)
                    return {
                        done: false,
                        value: `${url} is up, status: ${checkResult.status}`
                    }
                } catch (err) {
                    return {
                        done: false,
                        value: `${url} is down, error: ${err.message}`
                    }
                }
            }
        }
    }
}

让我们分析一下前面的代码最重要的部分:

  1. CheckUrls 类构造函数将 URL 列表作为输入。 既然我们现在知道如何使用迭代器和可迭代对象,我们可以说这个 URL 列表可以是任何可迭代对象。

  2. 在我们的 @@asyncIterator 方法中,我们从 this.urls 对象中获取一个迭代器,正如我们刚才所说,它应该是一个可迭代的。 我们可以通过简单地调用它的 @@iterable 方法来做到这一点。

  3. 请注意 next() 方法现在是一个异步函数。 这意味着它将始终按照异步可迭代协议的要求返回一个承诺。

  4. 在 next() 方法中,我们使用 urlsIterator 获取列表中的下一个 URL,除非没有更多的 URL,在这种情况下,我们只需返回 {done: true}。

  5. 请注意我们现在如何使用 await 指令异步获取发送到当前 URL 的 HEAD 请求的结果。

现在,让我们使用前面提到的 for wait…​of 语法来迭代 CheckUrls 对象:

import { CheckUrls } from './checkUrls.js'
async function main () {
    const checkUrls = new CheckUrls([
        'https://nodejsdesignpatterns.com',
        'https://example.com',
        'https://mustbedownforsurehopefully.com'
    ])
    for await (const status of checkUrls) {
        console.log(status)
    }
}

main()

正如我们所看到的,for wait…​of 语法是一种非常直观的迭代异步可迭代对象的方法,并且正如我们稍后将看到的,它可以与一些有趣的内置可迭代对象结合使用以获得替代方案 访问异步信息的新方法。

如果 for wait…​of 循环(及其同步版本)因中断、返回或异常而过早中断,则将调用迭代器的可选 return() 方法。 这可用于立即执行通常在迭代完成时执行的任何清理任务。

异步生成器

除了异步迭代器之外,我们还可以拥有异步生成器。 要定义异步生成器函数,只需在函数定义前添加关键字 async 即可:

async function * generatorFunction() {
    // ...generator body
}

正如您可以想象的那样,异步生成器允许在其主体中使用 await 指令,并且其 next() 方法的返回值是一个承诺,该承诺解析为具有规范的 done 和 value 属性的对象。 这样,异步生成器对象也是有效的异步迭代器。 它们也是有效的异步迭代,因此可以在 for wait…​of 循环中使用。

为了演示异步生成器如何简化异步迭代器的实现,让我们将前面示例中看到的 CheckUrls 类转换为使用异步生成器:

export class CheckUrls {
    constructor (urls) {
        this.urls = urls
    }
    async * [Symbol.asyncIterator] () {
        for (const url of this.urls) {
            try {
                const checkResult = await superagent
                    .head(url)
                    .redirects(2)
                yield `${url} is up, status: ${checkResult.status}`
            } catch (err) {
                yield `${url} is down, error: ${err.message}`
            }
        }
    }
}

有趣的是,使用异步生成器代替裸异步迭代器使我们能够节省几行代码,并且生成的逻辑也更具可读性和明确性。

异步迭代器和 Node.js 流

如果我们停下来思考一下异步迭代器和 Node.js 可读流之间的关系,我们会惊讶地发现它们在目的和行为上是多么相似。 事实上,我们可以说异步迭代器确实是一个流构造,因为它们可以用来逐个处理异步资源的数据,就像可读流一样。

Stream.Readable 实现 @@asyncIterator 方法,使其成为异步可迭代对象,这并非巧合。 这为我们提供了一种额外的、甚至可能更直观的机制来从可读流中读取数据,这要归功于 for wait…​of 结构。

为了快速演示这一点,请考虑以下示例,其中我们获取当前进程的 stdin 流,并将其通过管道传输到 split() 转换流中,该流在找到换行符时将发出一个新块。 然后,我们使用 for wait…​of 循环迭代每一行:

import split from 'split2'

async function main () {
    const stream = process.stdin.pipe(split())
    for await (const line of stream) {
        console.log(`You wrote: ${line}`)
    }
}

main()

仅当我们按下 Return 键后,此示例代码才会打印回我们写入标准输入的内容。 要退出程序,只需按 Ctrl + C 即可。

正如我们所看到的,这种消费可读流的替代方式确实非常直观和紧凑。 前面的示例还向我们展示了迭代器和流这两种范式有多么相似。 它们非常相似,几乎可以无缝地互操作。 为了进一步证明这一点,只需考虑函数 stream.Readable.from(iterable, [options]) 将一个 iterable 作为参数,它可以是同步的也可以是异步的。 该函数将返回一个可读流,该流包装了所提供的可迭代对象,将其接口 “调整” 为可读流的接口(这也是适配器模式的一个很好的例子,我们已经在第 8 章 “结构设计模式” 中见过了。

那么,如果流和异步迭代器如此密切相关,那么您实际上应该使用哪一个呢? 一如既往,这取决于用例和许多其他因素; 但是,为了帮助您做出决定,以下是区分这两种结构的方面的列表:

  • 流是推式的,这意味着数据被流推入内部缓冲区,然后从缓冲区中使用。 异步迭代器默认是拉取的(除非迭代器显式实现了另一个逻辑),这意味着数据仅根据消费者的需求检索/生成。

  • 流更适合处理二进制数据,因为它们本身提供内部缓冲和背压。

  • 可以使用众所周知的简化API Pipe() 来组合流,而异步迭代器不提供任何标准化的组合方法。

我们也可以迭代 EventEmitter。 使用 events.on(emitter, eventName) 实用函数,我们实际上可以获得一个异步迭代,其迭代器将返回与指定 eventName 匹配的所有事件。

in the wild

迭代器,尤其是异步迭代器在 Node.js 生态系统中迅速流行。 事实上,在许多情况下,它们正在成为流的首选替代方案,并正在取代定制的迭代机制。

例如,包@databases/pg、@databases/mysql 和@databases/ sqlite 分别是用于访问Postgres、MySQL 和SQLite 数据库的流行库(更多信息请参见nodejsdp.link/atdatabases)。

它们都公开了一个名为 queryStream() 的函数,该函数返回一个异步可迭代对象,可用于轻松迭代查询结果。 例如:

for await (const record of db.queryStream(sql`SELECT * FROM my_table`))
{
    // do something with record
}

在内部,迭代器将自动处理查询结果的游标,因此我们所要做的只是使用 for wait…​of 结构进行循环。

另一个 API 严重依赖迭代器的库的例子是 Zeromq 包 (nodejsdp.link/npm-zeromq)。 当我们继续讨论其他行为模式时,我们将在下一节中看到有关中间件模式的详细示例。