Node.js中的异步生成器与异步迭代详解
前言
生成器函数在JavaScript中的出现早于引入async/await,这意味着在创建异步生成器(始终返回Promise且可以await的生成器)的同时,还引入了许多需要注意的事项。
今天,我们将研究异步生成器及其近亲——异步迭代。
注意:尽管这些概念应该适用于所有遵循现代规范的javascript,但本文中的所有代码都是针对Node.js10、12和14版开发和测试的。
异步生成器函数
看一下这个小程序:
//File:main.js constcreateGenerator=function*(){ yield'a' yield'b' yield'c' } constmain=()=>{ constgenerator=createGenerator() for(constitemofgenerator){ console.log(item) } } main()
这段代码定义了一个生成器函数,用该函数创建了一个生成器对象,然后用for...of循环遍历该生成器对象。相当标准的东西——尽管你绝不会在实际工作中用生成器来处理如此琐碎的事情。如果你不熟悉生成器和for...of循环,请看《Javascript生成器》和《ES6的循环和可迭代对象的》这两篇文章。在使用异步生成器之前,你需要对生成器和for...of循环有扎实的了解。
假设我们要在生成器函数中使用await,只要需要用async关键字声明函数,Node.js就支持这个功能。如果你不熟悉异步函数,那么请看《在现代JavaScript中编写异步任务》一文。
下面修改程序并在生成器中使用await。
//File:main.js constcreateGenerator=asyncfunction*(){ yieldawaitnewPromise((r)=>r('a')) yield'b' yield'c' } constmain=()=>{ constgenerator=createGenerator() for(constitemofgenerator){ console.log(item) } } main()
同样在实际工作中,你也不会这样做——你可能会await来自第三方API或库的函数。为了能让大家轻松掌握,我们的例子尽量保持简单。
如果尝试运行上述程序,则会遇到问题:
$nodemain.js /Users/alanstorm/Desktop/main.js:9 for(constitemofgenerator){ ^ TypeError:generatorisnotiterable
JavaScript告诉我们这个生成器是“不可迭代的”。乍一看,似乎使生成器函数异步也意味着它生成的生成器是不可迭代的。这有点令人困惑,因为生成器的目的是生成“以编程方式”可迭代的对象。
接下来搞清楚到底发生了什么。
检查生成器
如果你看了Javascript生成器[1]的可迭代对象。当对象具有next方法时,该对象将实现迭代器协议,并且该next方法返回带有value属性,done属性之一或同时带有value和done属性的对象。
如果用下面这段代码比较异步生成器函数与常规生成器函数返回的生成器对象:
//File:test-program.js constcreateGenerator=function*(){ yield'a' yield'b' yield'c' } constcreateAsyncGenerator=asyncfunction*(){ yieldawaitnewPromise((r)=>r('a')) yield'b' yield'c' } constmain=()=>{ constgenerator=createGenerator() constasyncGenerator=createAsyncGenerator() console.log('generator:',generator[Symbol.iterator]) console.log('asyncGenerator',asyncGenerator[Symbol.iterator]) } main()
则会看到,前者没有Symbol.iterator方法,而后者有。
$nodetest-program.js generator:[Function:[Symbol.iterator]] asyncGeneratorundefined
这两个生成器对象都有一个next方法。如果修改测试代码来调用这个next方法:
//File:test-program.js /*...*/ constmain=()=>{ constgenerator=createGenerator() constasyncGenerator=createAsyncGenerator() console.log('generator:',generator.next()) console.log('asyncGenerator',asyncGenerator.next()) } main()
则会看到另一个问题:
$nodetest-program.js generator:{value:'a',done:false} asyncGeneratorPromise{}
为了使对象可迭代,next方法需要返回带有value和done属性的对象。一个async函数将总是返回一个Promise对象。这个特性会带到用异步函数创建的生成器上——这些异步生成器始终会yield一个Promise对象。
这种行为使得async函数的生成器无法实现javascript迭代协议。
异步迭代
幸运的是有办法解决这个矛盾。如果看一看async生成器返回的构造函数或类
//File:test-program.js /*...*/ constmain=()=>{ constgenerator=createGenerator() constasyncGenerator=createAsyncGenerator() console.log('asyncGenerator',asyncGenerator) }
可以看到它是一个对象,其类型或类或构造函数是AsyncGenerator而不是Generator:
asyncGeneratorObject[AsyncGenerator]{}
尽管该对象有可能不是可迭代的,但它是异步可迭代的。
要想使对象能够异步迭代,它必须实现一个Symbol.asyncIterator方法。这个方法必须返回一个对象,该对象实现了异步版本的迭代器协议。也就是说,对象必须具有返回Promise的next方法,并且这个promise必须最终解析为带有done和value属性的对象。
一个AsyncGenerator对象满足所有这些条件。
这就留下了一个问题——我们怎样才能遍历一个不可迭代但可以异步迭代的对象?
forawait…of循环
只用生成器的next方法就可以手动迭代异步可迭代对象。(注意,这里的main函数现在是asyncmain——这样能够使我们在函数内部使用await)
//File:main.js constcreateAsyncGenerator=asyncfunction*(){ yieldawaitnewPromise((r)=>r('a')) yield'b' yield'c' } constmain=async()=>{ constasyncGenerator=createAsyncGenerator() letresult={done:false} while(!result.done){ result=awaitasyncGenerator.next() if(result.done){continue;} console.log(result.value) } } main()
但是,这不是最直接的循环机制。我既不喜欢while的循环条件,也不想手动检查result.done。另外,result.done变量必须同时存在于内部和外部块的作用域内。
幸运的是大多数(也许是所有?)支持异步迭代器的javascript实现也都支持特殊的forawait...of循环语法。例如:
constcreateAsyncGenerator=asyncfunction*(){ yieldawaitnewPromise((r)=>r('a')) yield'b' yield'c' } constmain=async()=>{ constasyncGenerator=createAsyncGenerator() forawait(constitemofasyncGenerator){ console.log(item) } } main()
如果运行上述代码,则会看到异步生成器与可迭代对象已被成功循环,并且在循环体中得到了Promise的完全解析值。
$nodemain.js
a
b
c
这个forawait...of循环更喜欢实现了异步迭代器协议的对象。但是你可以用它遍历任何一种可迭代对象。
forawait(constitemof[1,2,3]){ console.log(item) }
当你使用forawait时,Node.js将会首先在对象上寻找Symbol.asyncIterator方法。如果找不到,它将回退到使用Symbol.iterator的方法。
非线性代码执行
与await一样,forawait循环会将非线性代码执行引入程序中。也就是说,你的代码将会以和编写的代码不同的顺序运行。
当你的程序第一次遇到forawait循环时,它将在你的对象上调用next。
该对象将yield一个promise,然后代码的执行将会离开你的async函数,并且你的程序将继续在该函数之外执行。
一旦你的promise得到解决,代码执行将会使用这个值返回到循环体。
当循环结束并进行下一个行程时,Node.js将在对象上调用next。该调用会产生另一个promise,代码执行将会再次离开你的函数。重复这种模式,直到Promise解析为done为true的对象,然后在forawait循环之后继续执行代码。
下面的例子可以说明一点:
letcount=0 constgetCount=()=>{ count++ return`${count}.` } constcreateAsyncGenerator=asyncfunction*(){ console.log(getCount()+'enteringcreateAsyncGenerator') console.log(getCount()+'abouttoyielda') yieldawaitnewPromise((r)=>r('a')) console.log(getCount()+'re-enteringcreateAsyncGenerator') console.log(getCount()+'abouttoyieldb') yield'b' console.log(getCount()+'re-enteringcreateAsyncGenerator') console.log(getCount()+'abouttoyieldc') yield'c' console.log(getCount()+'re-enteringcreateAsyncGenerator') console.log(getCount()+'exitingcreateAsyncGenerator') } constmain=async()=>{ console.log(getCount()+'enteringmain') constasyncGenerator=createAsyncGenerator() console.log(getCount()+'startingforawaitloop') forawait(constitemofasyncGenerator){ console.log(getCount()+'enteringforawaitloop') console.log(getCount()+item) console.log(getCount()+'exitingforawaitloop') } console.log(getCount()+'donewithforawaitloop') console.log(getCount()+'leavingmain') } console.log(getCount()+'beforecallingmain') main() console.log(getCount()+'aftercallingmain')
这段代码你用了编号的日志记录语句,可让你跟踪其执行情况。作为练习,你需要自己运行程序然后查看执行结果是怎样的。
如果你不知道它的工作方式,就会使程序的执行产生混乱,但异步迭代的确是一项强大的技术。
总结
到此这篇关于Node.js中异步生成器与异步迭代的文章就介绍到这了,更多相关Node.js异步生成器与异步迭代内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!