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


Posted in Javascript onJanuary 31, 2021

前言

生成器函数在 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异步生成器与异步迭代内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
jquery 实现input输入什么div图层显示什么
Jun 15 Javascript
JavaScript中停止执行setInterval和setTimeout事件的方法
May 14 Javascript
推荐阅读的js快速判断IE浏览器(兼容IE10与IE11)
Dec 13 Javascript
简单介绍JavaScript数据类型之隐式类型转换
Dec 28 Javascript
深入解析JavaScript中的立即执行函数
May 21 Javascript
轻松掌握JavaScript中的Math object数学对象
May 26 Javascript
浅谈jquery之on()绑定事件和off()解除绑定事件
Oct 26 Javascript
vue与bootstrap实现时间选择器的示例代码
Aug 26 Javascript
关于JavaScript语句后面的分号问题
Dec 07 Javascript
解决layui中table异步数据请求不支持自定义返回数据格式的问题
Aug 19 Javascript
JS函数动态传递参数的方法分析【基于arguments对象】
Jun 05 Javascript
JS 自执行函数原理及用法
Aug 05 Javascript
Vite和Vue CLI的优劣
Jan 30 #Vue.js
如何使用RoughViz可视化Vue.js中的草绘图表
Jan 30 #Vue.js
Element-ui 自带的两种远程搜索(模糊查询)用法讲解
Jan 29 #Javascript
小程序实现列表倒计时功能
Jan 29 #Javascript
JS相册图片抖动放大展示效果的示例代码
Jan 29 #Javascript
vue监听键盘事件的相关总结
Jan 29 #Vue.js
javascript实现拼图游戏
Jan 29 #Javascript
You might like
编写漂亮的代码 - 将后台程序与前端程序分开
2008/04/23 PHP
PHP中将字符串转化为整数(int) intval() printf() 性能测试
2020/03/20 PHP
CI操作cookie的方法分析(基于helper类库)
2016/03/28 PHP
php批量转换文件夹下所有文件编码的函数类
2017/08/06 PHP
PHP有序表查找之插值查找算法示例
2018/02/10 PHP
javascript Onunload与Onbeforeunload使用小结
2009/12/31 Javascript
十个优秀的Ajax/Javascript实例网站收集
2010/03/31 Javascript
JS HTML5拖拽上传图片预览
2016/07/18 Javascript
AngularJS ng-mousedown 指令
2016/08/02 Javascript
学习Angular中作用域需要注意的坑
2016/08/17 Javascript
WEB开发之注册页面验证码倒计时代码的实现
2016/12/15 Javascript
Form表单上传文件(type=&quot;file&quot;)的使用
2017/08/03 Javascript
微信小程序开发教程之增加mixin扩展
2017/08/09 Javascript
微信小程序WebSocket实现聊天对话功能
2018/07/06 Javascript
微信小程序http连接访问解决方案的示例
2018/11/05 Javascript
JavaScript算法学习之冒泡排序和选择排序
2019/11/02 Javascript
vue 中的动态传参和query传参操作
2020/11/09 Javascript
[02:36]DOTA2英雄基础教程 帕格纳
2014/01/20 DOTA
[02:57]DOTA2亚洲邀请赛小组赛第四日 赛事回顾
2015/02/02 DOTA
3分钟学会一个Python小技巧
2018/11/23 Python
django的csrf实现过程详解
2019/07/26 Python
python框架flask入门之环境搭建及开启调试
2020/06/07 Python
在vscode中启动conda虚拟环境的思路详解
2020/12/25 Python
css3实现椭圆轨迹旋转的示例代码
2018/10/29 HTML / CSS
Bootstrap File Input文件上传组件
2020/12/01 HTML / CSS
Bonami斯洛伐克:购买家具和家居饰品
2019/07/02 全球购物
集团公司人力资源部岗位职责
2014/01/03 职场文书
生日宴会策划方案
2014/06/03 职场文书
工作疏忽、懈怠的检讨书
2014/09/11 职场文书
信访工作汇报材料
2014/10/27 职场文书
病人慰问信范文
2015/02/15 职场文书
大学生违纪检讨书范文
2015/05/07 职场文书
MySQL 视图(View)原理解析
2021/05/19 MySQL
Win11无法访问设备和打印机 如何解决页面空白
2022/04/09 数码科技
使用Python获取字典键对应值的方法
2022/04/26 Python
SQL Server中的逻辑函数介绍
2022/05/25 SQL Server