Node.js中的異步生成器與異步迭代詳解

前言

生成器函數在 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異步生成器與異步迭代內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: