Node.js中的异步生成器与异步迭代详解

 更新时间:2021年1月31日 15:00  点击:1768

前言

生成器函数在 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 对象满足所有这些条件。

这就留下了一个问题——我们怎样才能遍历一个不可迭代但可以异步迭代的对象?

for await … of 循环

只用生成器的 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异步生成器与异步迭代内容请搜索猪先飞以前的文章或继续浏览下面的相关文章希望大家以后多多支持猪先飞!

[!--infotagslink--]

相关文章

  • node.JS md5加密中文与php结果不一致怎么办

    这次文章要给大家介绍的是node.JS md5加密中文与php结果不一致怎么办,不知道具体解决办法的下面跟小编一起来看看。 因项目需要,需要Node.js与PHP做接口调用,发现nod...2017-07-06
  • NODE.JS加密模块CRYPTO常用方法介绍

    使用require('crypto')调用加密模块。加密模块需要底层系统提供OpenSSL的支持。它提供了一种安全凭证的封装方式,可以用于HTTPS安全网络以及普通HTTP连接。该模块还提供了一套针对OpenSSL的hash(哈希),hmac(密钥哈希),cipher...2014-06-07
  • 浅谈node.js中async异步编程

    1.什么是异步编程? 异步编程是指由于异步I/O等因素,无法同步获得执行结果时, 在回调函数中进行下一步操作的代码编写风格,常见的如setTimeout函数、ajax请求等等。示例: for (var i = 1; i <= 3; i++) {setTimeout(functi...2015-10-23
  • node.js+express留言板功能实现示例

    本文介绍基于nodejs+express+art-template的留言板功能。包含列表界面、添加界面和发送留言功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-21
  • 安装使用Mongoose配合Node.js操作MongoDB的基础教程

    这篇文章主要介绍了安装使用Mongoose来让Node.js操作MongoDB的基础教程,前端js+后端node+js操作MongoDB正是所谓最流行的一种JavaScript全栈开发方案,需要的朋友可以参考下...2016-03-03
  • 利用Node.js获取项目根目录的小技巧

    这篇文章介绍的是一个小技巧来获取node.js项目根目录,这个技巧非常实用。有需要的朋友们可以参考借鉴,下面来一起看看吧。...2016-10-02
  • node.js从数据库获取数据

    这篇文章主要为大家详细介绍了node.js从数据库获取数据的具体代码,nodejs可以获取具体某张数据表信息,感兴趣的朋友可以参考一下...2016-05-09
  • node.js [superAgent] 请求使用示例

    post请求:复制代码 代码如下: request.post('/api/pet') .end(function(resp,err){ if (resp.body.status===200) { alert('yay got ' + JSON.stringify(res.body)); } else { return nex...2015-03-15
  • node.js如何操作MySQL数据库

    这篇文章主要介绍了node.js如何操作MySQL数据库,帮助大家更好的进行web开发,感兴趣的朋友可以了解下...2020-10-29
  • 比较node.js和Deno

    这篇文章主要介绍了node.js和Deno的区别,对deno感兴趣的同学,可以参考下...2021-04-27
  • 三种Node.js写文件的方式

    这篇文章主要为大家详细介绍了三种Node.js写文件的方式,感兴趣的小伙伴们可以参考一下...2016-03-10
  • Node.js之http模块的用法

    这篇文章主要介绍了Node.js之http模块的用法,对Node.js感兴趣的同学,可以参考下...2021-04-25
  • node.js 全局变量的具体使用

    这篇文章主要介绍了node.js 全局变量的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-04-15
  • node.js爬虫爬取拉勾网职位信息

    本篇文章主要介绍了node.js爬虫爬取拉勾网职位信息的方法。具有很好的参考价值。下面跟着小编一起来看下吧 用node.js写了一个简单的小爬虫,用来爬取拉勾网上的招...2017-07-06
  • Webpack 实现 Node.js 代码热替换

    这两天为了这个问题, Gitter 上问, Twitter 上问, GitHub 上问, 两天没反应 原来写博客的 jlongster 不理我, 我也不知道 Webpack 作者的联系方式 最后在 Gitter 上发的消息他似乎看到了, 就粗略地解释了一遍, 醍醐灌...2015-10-23
  • 通过node-mysql搭建Windows+Node.js+MySQL环境的教程

    这篇文章主要介绍了通过node-mysql搭建Windows+Node.js+MySQL环境的教程,node-mysql是JavaScript编写的一个Node的MySQL驱动,需要的朋友可以参考下...2016-03-03
  • Linux下为Node.js程序配置MySQL或Oracle数据库的方法

    这篇文章主要介绍了Linux下为Node.js程序配置MySQL或Oracle数据库的方法,这里默认已经装配好了Node环境然后我们利用npm包管理工具来进行配置,需要的朋友可以参考下...2016-03-22
  • node.js中 stream使用教程

    Stream 是一个抽象接口,Node 中有很多对象实现了这个接口。例如,对http 服务器发起请求的request 对象就是一个 Stream,还有stdout(标准输出)。...2016-09-01
  • 教你如何在Node.js中使用jQuery

    本文给大家分享的是如何在Node.js中使用jQuery的方法,包含步骤以及出错的处理,非常的详细,有需要的小伙伴可以参考下...2016-09-01
  • node.js爬虫框架node-crawler初体验

    这篇文章主要介绍了node.js爬虫框架node-crawler的相关资料,帮助大家利用node.js进行爬虫,感兴趣的朋友可以了解下...2020-10-29