生成器函数在 JavaScript 中的出现早于引入 async/await,这意味着在创建异步生成器(始终返回 Promise 且可以 await 的生成器)的同时,还引入了许多需要注意的事项。
今天,我们将研究异步生成器及其近亲——异步迭代。
注意:尽管这些概念应该适用于所有遵循现代规范的 javascript,但本文中的所有代码都是针对 Node.js 10、12 和 14 版开发和测试的。
看一下这个小程序:
// File: main.js const createGenerator = function*(){ yield 'a' yield 'b' yield 'c' } const main = () => { const generator = createGenerator() for (const item of generator) { console.log(item) } } main()
这段代码定义了一个生成器函数,用该函数创建了一个生成器对象,然后用 for ... of 循环遍历该生成器对象。相当标准的东西——尽管你绝不会在实际工作中用生成器来处理如此琐碎的事情。如果你不熟悉生成器和 for ... of 循环,请看《Javascript 生成器》 和 《ES6 的循环和可迭代对象的》 这两篇文章。在使用异步生成器之前,你需要对生成器和 for ... of 循环有扎实的了解。
假设我们要在生成器函数中使用 await,只要需要用 async 关键字声明函数,Node.js 就支持这个功能。如果你不熟悉异步函数,那么请看 《在现代 JavaScript 中编写异步任务》一文。
下面修改程序并在生成器中使用 await。
// File: main.js const createGenerator = async function*(){ yield await new Promise((r) => r('a')) yield 'b' yield 'c' } const main = () => { const generator = createGenerator() for (const item of generator) { console.log(item) } } main()
同样在实际工作中,你也不会这样做——你可能会 await 来自第三方 API 或库的函数。为了能让大家轻松掌握,我们的例子尽量保持简单。
如果尝试运行上述程序,则会遇到问题:
$ node main.js /Users/alanstorm/Desktop/main.js:9 for (const item of generator) { ^ TypeError: generator is not iterable
JavaScript 告诉我们这个生成器是“不可迭代的”。乍一看,似乎使生成器函数异步也意味着它生成的生成器是不可迭代的。这有点令人困惑,因为生成器的目的是生成“以编程方式”可迭代的对象。
接下来搞清楚到底发生了什么。
如果你看了 Javascript 生成器[1]的可迭代对象。当对象具有 next 方法时,该对象将实现迭代器协议,并且该 next 方法返回带有 value 属性,done 属性之一或同时带有 value 和 done 属性的对象。
如果用下面这段代码比较异步生成器函数与常规生成器函数返回的生成器对象:
// File: test-program.js const createGenerator = function*(){ yield 'a' yield 'b' yield 'c' } const createAsyncGenerator = async function*(){ yield await new Promise((r) => r('a')) yield 'b' yield 'c' } const main = () => { const generator = createGenerator() const asyncGenerator = createAsyncGenerator() console.log('generator:',generator[Symbol.iterator]) console.log('asyncGenerator',asyncGenerator[Symbol.iterator]) } main()
则会看到,前者没有 Symbol.iterator 方法,而后者有。
$ node test-program.js generator: [Function: [Symbol.iterator]] asyncGenerator undefined
这两个生成器对象都有一个 next 方法。如果修改测试代码来调用这个 next 方法:
// File: test-program.js /* ... */ const main = () => { const generator = createGenerator() const asyncGenerator = createAsyncGenerator() console.log('generator:',generator.next()) console.log('asyncGenerator',asyncGenerator.next()) } main()
则会看到另一个问题:
$ node test-program.js generator: { value: 'a', done: false } asyncGenerator Promise { <pending> }
为了使对象可迭代,next 方法需要返回带有 value 和 done 属性的对象。一个 async 函数将总是返回一个 Promise 对象。这个特性会带到用异步函数创建的生成器上——这些异步生成器始终会 yield 一个 Promise 对象。
这种行为使得 async 函数的生成器无法实现 javascript 迭代协议。
幸运的是有办法解决这个矛盾。如果看一看 async 生成器返回的构造函数或类
// File: test-program.js /* ... */ const main = () => { const generator = createGenerator() const asyncGenerator = createAsyncGenerator() console.log('asyncGenerator',asyncGenerator) }
可以看到它是一个对象,其类型或类或构造函数是 AsyncGenerator 而不是 Generator:
asyncGenerator Object [AsyncGenerator] {}
尽管该对象有可能不是可迭代的,但它是异步可迭代的。
要想使对象能够异步迭代,它必须实现一个 Symbol.asyncIterator 方法。这个方法必须返回一个对象,该对象实现了异步版本的迭代器协议。也就是说,对象必须具有返回 Promise 的 next 方法,并且这个 promise 必须最终解析为带有 done 和 value 属性的对象。
一个 AsyncGenerator 对象满足所有这些条件。
这就留下了一个问题——我们怎样才能遍历一个不可迭代但可以异步迭代的对象?
只用生成器的 next 方法就可以手动迭代异步可迭代对象。(注意,这里的 main 函数现在是 async main ——这样能够使我们在函数内部使用 await)
// File: main.js const createAsyncGenerator = async function*(){ yield await new Promise((r) => r('a')) yield 'b' yield 'c' } const main = async () => { const asyncGenerator = createAsyncGenerator() let result = {done:false} while(!result.done) { result = await asyncGenerator.next() if(result.done) { continue; } console.log(result.value) } } main()
但是,这不是最直接的循环机制。我既不喜欢 while 的循环条件,也不想手动检查 result.done。另外, result.done 变量必须同时存在于内部和外部块的作用域内。
幸运的是大多数(也许是所有?)支持异步迭代器的 javascript 实现也都支持特殊的 for await ... of 循环语法。例如:
const createAsyncGenerator = async function*(){ yield await new Promise((r) => r('a')) yield 'b' yield 'c' } const main = async () => { const asyncGenerator = createAsyncGenerator() for await(const item of asyncGenerator) { console.log(item) } } main()
如果运行上述代码,则会看到异步生成器与可迭代对象已被成功循环,并且在循环体中得到了 Promise 的完全解析值。
$ node main.js
a
b
c
这个 for await ... of 循环更喜欢实现了异步迭代器协议的对象。但是你可以用它遍历任何一种可迭代对象。
for await(const item of [1,2,3]) { console.log(item) }
当你使用 for await 时,Node.js 将会首先在对象上寻找 Symbol.asyncIterator 方法。如果找不到,它将回退到使用 Symbol.iterator 的方法。
与 await 一样,for await 循环会将非线性代码执行引入程序中。也就是说,你的代码将会以和编写的代码不同的顺序运行。
当你的程序第一次遇到 for await 循环时,它将在你的对象上调用 next。
该对象将 yield 一个 promise,然后代码的执行将会离开你的 async 函数,并且你的程序将继续在该函数之外执行。
一旦你的 promise 得到解决,代码执行将会使用这个值返回到循环体。
当循环结束并进行下一个行程时,Node.js 将在对象上调用 next。该调用会产生另一个 promise,代码执行将会再次离开你的函数。重复这种模式,直到 Promise 解析为 done 为 true 的对象,然后在 for await 循环之后继续执行代码。
下面的例子可以说明一点:
let count = 0 const getCount = () => { count++ return `${count}. ` } const createAsyncGenerator = async function*() { console.log(getCount() + 'entering createAsyncGenerator') console.log(getCount() + 'about to yield a') yield await new Promise((r)=>r('a')) console.log(getCount() + 're-entering createAsyncGenerator') console.log(getCount() + 'about to yield b') yield 'b' console.log(getCount() + 're-entering createAsyncGenerator') console.log(getCount() + 'about to yield c') yield 'c' console.log(getCount() + 're-entering createAsyncGenerator') console.log(getCount() + 'exiting createAsyncGenerator') } const main = async () => { console.log(getCount() + 'entering main') const asyncGenerator = createAsyncGenerator() console.log(getCount() + 'starting for await loop') for await(const item of asyncGenerator) { console.log(getCount() + 'entering for await loop') console.log(getCount() + item) console.log(getCount() + 'exiting for await loop') } console.log(getCount() + 'done with for await loop') console.log(getCount() + 'leaving main') } console.log(getCount() + 'before calling main') main() console.log(getCount() + 'after calling main')
这段代码你用了编号的日志记录语句,可让你跟踪其执行情况。作为练习,你需要自己运行程序然后查看执行结果是怎样的。
如果你不知道它的工作方式,就会使程序的执行产生混乱,但异步迭代的确是一项强大的技术。
到此这篇关于Node.js中异步生成器与异步迭代的文章就介绍到这了,更多相关Node.js异步生成器与异步迭代内容请搜索小牛知识库以前的文章或继续浏览下面的相关文章希望大家以后多多支持小牛知识库!
JavaScript中有没有已知的技巧来区分和之间的区别,而不触发迭代? 我正在尝试实现以下类型检查器: 我知道调用会告诉我们这一点,但在无法触发迭代时我需要它。 此外,即使我在TypeScript中给出了示例,我也需要在运行时严格检查它。
问题内容: 我已经将kriskowal的Q库用于一个项目(Web刮板/人类活动模拟器),并且熟悉了Promise,将其返回并解决/拒绝它们,并且该库的基本异步控制流方法和错误抛出/捕获机制已得到证明。必要。 我遇到了一些问题。我的电话和回叫有形成金字塔的异常趋势。有时是出于范围限制的原因,而其他时候是为了保证一定顺序的事件。(我想我也许可以通过重构来解决其中的一些问题,但是今后我想完全避免“回调地
问题内容: 我们正在与节点合作,主要用于内部项目,并了解使用该技术的最佳方法。 并非来自特定的异步背景,学习曲线可能是一个挑战,但是我们已经习惯了框架和学习过程。 使我们两极分化的是,何时才是使用同步代码与异步代码的最佳时间。我们目前使用的规则是,如果任何东西与IO进行交互,那么它必须通过回调或事件发射器(即给定的)是异步的,但是可以将任何未使用IO的其他项构造为同步函数(此方法还将取决于函数本身
异步迭代允许我们对按需通过异步请求而得到的数据进行迭代。例如,我们通过网络分段(chunk-by-chunk)下载数据时。异步生成器(generator)使这一步骤更加方便。 首先,让我们来看一个简单的示例以掌握语法,然后再看一个实际用例。 回顾可迭代对象 让我们回顾一下可迭代对象的相关内容。 假设我们有一个对象,例如下面的 range: let range = { from: 1, to
问题内容: 我该如何做这项工作 我试图从异步之一获取同步功能,我需要它来使用FreeTds异步查询作为同步之一 问题答案: 使用deasync-用C ++编写的模块,它将Node.js事件循环暴露给JavaScript。该模块还公开了一个函数,该函数阻止后续代码,但不阻止整个线程,也不引起繁忙的等待。您可以将函数放入循环中:
问题内容: 我想对不带数字键()的mongoDB集合执行迭代。集合只有一个随机字符串作为_id,并且集合的大小很大,因此使用来将整个文档加载到RAM 上不是可行的选择。另外,我想对每个元素执行异步任务。的使用或者,是因为任务的异步性质的限制。我尝试使用上述方法运行任务,但它确实与异步任务冲突,返回了未完成的承诺而不是正确的结果。 例 我怎样才能仅使用mongoDB集合进行迭代? 问题答案: 该方法