Node.js中的異步生成器與異步迭代詳解
前言
生成器函數(shù)在 JavaScript 中的出現(xiàn)早于引入 async/await,這意味著在創(chuàng)建異步生成器(始終返回 Promise 且可以 await 的生成器)的同時(shí),還引入了許多需要注意的事項(xiàng)。
今天,我們將研究異步生成器及其近親——異步迭代。
注意:盡管這些概念應(yīng)該適用于所有遵循現(xiàn)代規(guī)范的 javascript,但本文中的所有代碼都是針對(duì) Node.js 10、12 和 14 版開(kāi)發(fā)和測(cè)試的。
異步生成器函數(shù)
看一下這個(gè)小程序:
// 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()
這段代碼定義了一個(gè)生成器函數(shù),用該函數(shù)創(chuàng)建了一個(gè)生成器對(duì)象,然后用 for ... of 循環(huán)遍歷該生成器對(duì)象。相當(dāng)標(biāo)準(zhǔn)的東西——盡管你絕不會(huì)在實(shí)際工作中用生成器來(lái)處理如此瑣碎的事情。如果你不熟悉生成器和 for ... of 循環(huán),請(qǐng)看《Javascript 生成器》 和 《ES6 的循環(huán)和可迭代對(duì)象的》 這兩篇文章。在使用異步生成器之前,你需要對(duì)生成器和 for ... of 循環(huán)有扎實(shí)的了解。
假設(shè)我們要在生成器函數(shù)中使用 await,只要需要用 async 關(guān)鍵字聲明函數(shù),Node.js 就支持這個(gè)功能。如果你不熟悉異步函數(shù),那么請(qǐng)看 《在現(xiàn)代 JavaScript 中編寫(xiě)異步任務(wù)》一文。
下面修改程序并在生成器中使用 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()
同樣在實(shí)際工作中,你也不會(huì)這樣做——你可能會(huì) await 來(lái)自第三方 API 或庫(kù)的函數(shù)。為了能讓大家輕松掌握,我們的例子盡量保持簡(jiǎn)單。
如果嘗試運(yùn)行上述程序,則會(huì)遇到問(wèn)題:
$ node main.js
/Users/alanstorm/Desktop/main.js:9
for (const item of generator) {
^
TypeError: generator is not iterable
JavaScript 告訴我們這個(gè)生成器是“不可迭代的”。乍一看,似乎使生成器函數(shù)異步也意味著它生成的生成器是不可迭代的。這有點(diǎn)令人困惑,因?yàn)樯善鞯哪康氖巧伞耙跃幊谭绞健笨傻膶?duì)象。
接下來(lái)搞清楚到底發(fā)生了什么。
檢查生成器
如果你看了 Javascript 生成器[1]的可迭代對(duì)象。當(dāng)對(duì)象具有 next 方法時(shí),該對(duì)象將實(shí)現(xiàn)迭代器協(xié)議,并且該 next 方法返回帶有 value 屬性,done 屬性之一或同時(shí)帶有 value 和 done 屬性的對(duì)象。
如果用下面這段代碼比較異步生成器函數(shù)與常規(guī)生成器函數(shù)返回的生成器對(duì)象:
// 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()
則會(huì)看到,前者沒(méi)有 Symbol.iterator 方法,而后者有。
$ node test-program.js generator: [Function: [Symbol.iterator]] asyncGenerator undefined
這兩個(gè)生成器對(duì)象都有一個(gè) next 方法。如果修改測(cè)試代碼來(lái)調(diào)用這個(gè) next 方法:
// File: test-program.js
/* ... */
const main = () => {
const generator = createGenerator()
const asyncGenerator = createAsyncGenerator()
console.log('generator:',generator.next())
console.log('asyncGenerator',asyncGenerator.next())
}
main()
則會(huì)看到另一個(gè)問(wèn)題:
$ node test-program.js
generator: { value: 'a', done: false }
asyncGenerator Promise { <pending> }
為了使對(duì)象可迭代,next 方法需要返回帶有 value 和 done 屬性的對(duì)象。一個(gè) async 函數(shù)將總是返回一個(gè) Promise 對(duì)象。這個(gè)特性會(huì)帶到用異步函數(shù)創(chuàng)建的生成器上——這些異步生成器始終會(huì) yield 一個(gè) Promise 對(duì)象。
這種行為使得 async 函數(shù)的生成器無(wú)法實(shí)現(xiàn) javascript 迭代協(xié)議。
異步迭代
幸運(yùn)的是有辦法解決這個(gè)矛盾。如果看一看 async 生成器返回的構(gòu)造函數(shù)或類(lèi)
// File: test-program.js
/* ... */
const main = () => {
const generator = createGenerator()
const asyncGenerator = createAsyncGenerator()
console.log('asyncGenerator',asyncGenerator)
}
可以看到它是一個(gè)對(duì)象,其類(lèi)型或類(lèi)或構(gòu)造函數(shù)是 AsyncGenerator 而不是 Generator:
asyncGenerator Object [AsyncGenerator] {}
盡管該對(duì)象有可能不是可迭代的,但它是異步可迭代的。
要想使對(duì)象能夠異步迭代,它必須實(shí)現(xiàn)一個(gè) Symbol.asyncIterator 方法。這個(gè)方法必須返回一個(gè)對(duì)象,該對(duì)象實(shí)現(xiàn)了異步版本的迭代器協(xié)議。也就是說(shuō),對(duì)象必須具有返回 Promise 的 next 方法,并且這個(gè) promise 必須最終解析為帶有 done 和 value 屬性的對(duì)象。
一個(gè) AsyncGenerator 對(duì)象滿(mǎn)足所有這些條件。
這就留下了一個(gè)問(wèn)題——我們?cè)鯓硬拍鼙闅v一個(gè)不可迭代但可以異步迭代的對(duì)象?
for await … of 循環(huán)
只用生成器的 next 方法就可以手動(dòng)迭代異步可迭代對(duì)象。(注意,這里的 main 函數(shù)現(xiàn)在是 async main ——這樣能夠使我們?cè)诤瘮?shù)內(nèi)部使用 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()
但是,這不是最直接的循環(huán)機(jī)制。我既不喜歡 while 的循環(huán)條件,也不想手動(dòng)檢查 result.done。另外, result.done 變量必須同時(shí)存在于內(nèi)部和外部塊的作用域內(nèi)。
幸運(yùn)的是大多數(shù)(也許是所有?)支持異步迭代器的 javascript 實(shí)現(xiàn)也都支持特殊的 for await ... of 循環(huán)語(yǔ)法。例如:
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()
如果運(yùn)行上述代碼,則會(huì)看到異步生成器與可迭代對(duì)象已被成功循環(huán),并且在循環(huán)體中得到了 Promise 的完全解析值。
$ node main.js
a
b
c
這個(gè) for await ... of 循環(huán)更喜歡實(shí)現(xiàn)了異步迭代器協(xié)議的對(duì)象。但是你可以用它遍歷任何一種可迭代對(duì)象。
for await(const item of [1,2,3]) {
console.log(item)
}
當(dāng)你使用 for await 時(shí),Node.js 將會(huì)首先在對(duì)象上尋找 Symbol.asyncIterator 方法。如果找不到,它將回退到使用 Symbol.iterator 的方法。
非線(xiàn)性代碼執(zhí)行
與 await 一樣,for await 循環(huán)會(huì)將非線(xiàn)性代碼執(zhí)行引入程序中。也就是說(shuō),你的代碼將會(huì)以和編寫(xiě)的代碼不同的順序運(yùn)行。
當(dāng)你的程序第一次遇到 for await 循環(huán)時(shí),它將在你的對(duì)象上調(diào)用 next。
該對(duì)象將 yield 一個(gè) promise,然后代碼的執(zhí)行將會(huì)離開(kāi)你的 async 函數(shù),并且你的程序?qū)⒗^續(xù)在該函數(shù)之外執(zhí)行。
一旦你的 promise 得到解決,代碼執(zhí)行將會(huì)使用這個(gè)值返回到循環(huán)體。
當(dāng)循環(huán)結(jié)束并進(jìn)行下一個(gè)行程時(shí),Node.js 將在對(duì)象上調(diào)用 next。該調(diào)用會(huì)產(chǎn)生另一個(gè) promise,代碼執(zhí)行將會(huì)再次離開(kāi)你的函數(shù)。重復(fù)這種模式,直到 Promise 解析為 done 為 true 的對(duì)象,然后在 for await 循環(huán)之后繼續(xù)執(zhí)行代碼。
下面的例子可以說(shuō)明一點(diǎn):
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')
這段代碼你用了編號(hào)的日志記錄語(yǔ)句,可讓你跟蹤其執(zhí)行情況。作為練習(xí),你需要自己運(yùn)行程序然后查看執(zhí)行結(jié)果是怎樣的。
如果你不知道它的工作方式,就會(huì)使程序的執(zhí)行產(chǎn)生混亂,但異步迭代的確是一項(xiàng)強(qiáng)大的技術(shù)。
總結(jié)
到此這篇關(guān)于Node.js中異步生成器與異步迭代的文章就介紹到這了,更多相關(guān)Node.js異步生成器與異步迭代內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Node.js中調(diào)用mysql存儲(chǔ)過(guò)程示例
這篇文章主要介紹了Node.js中調(diào)用mysql存儲(chǔ)過(guò)程示例,本文在windows環(huán)境測(cè)試通過(guò),本文一并給出了創(chuàng)建數(shù)據(jù)庫(kù)、錄入數(shù)據(jù)、創(chuàng)建存儲(chǔ)過(guò)程、調(diào)用存儲(chǔ)過(guò)程等例子,需要的朋友可以參考下2014-12-12
13 個(gè)npm 快速開(kāi)發(fā)技巧(推薦)
這篇文章主要介紹了13 個(gè)npm 快速開(kāi)發(fā)技巧,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-07-07
安裝使用Mongoose配合Node.js操作MongoDB的基礎(chǔ)教程
這篇文章主要介紹了安裝使用Mongoose來(lái)讓Node.js操作MongoDB的基礎(chǔ)教程,前端js+后端node+js操作MongoDB正是所謂最流行的一種JavaScript全棧開(kāi)發(fā)方案,需要的朋友可以參考下2016-03-03
npm?install安裝失敗常見(jiàn)問(wèn)題的解決辦法小結(jié)
有時(shí)候前端安裝npm install 安裝包總是安裝不上,下面這篇文章主要給大家介紹了關(guān)于npm?install安裝失敗常見(jiàn)問(wèn)題的解決辦法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
node以及npm版本不對(duì)應(yīng)出錯(cuò)的完美解決方法
最近項(xiàng)目用到了node和npm,查看一下當(dāng)前版本,發(fā)現(xiàn)有報(bào)錯(cuò),下面這篇文章主要給大家介紹了關(guān)于node以及npm版本不對(duì)應(yīng)出錯(cuò)的完美解決方法,文中通過(guò)圖文介紹的非常詳細(xì),需要的朋友可以參考下2023-05-05
在Linux系統(tǒng)上升級(jí)Node.js遇到GLIBC依賴(lài)問(wèn)題的多種解決方案
在現(xiàn)代 Web 開(kāi)發(fā)和 DevOps 實(shí)踐中,Node.js 是一個(gè)不可或缺的工具,在升級(jí) Node.js 版本時(shí),尤其是在較舊的 Linux 系統(tǒng)上,可能會(huì)遇到一些依賴(lài)庫(kù)不兼容的問(wèn)題,特別是與 GLIBC 和 GLIBCXX 相關(guān)的錯(cuò)誤,本文將詳細(xì)介紹如何解決這個(gè)依賴(lài)問(wèn)題,需要的朋友可以參考下2025-01-01

