生成器函數在JavaScript 中的出現早於引入async/await,這表示在建立非同步產生器(始終返回Promise
且可以 await
的生成器)的同時,也引入了許多需要注意的事項。
今天,我們將研究非同步產生器及其近親-非同步迭代。
注意:儘管這些概念應該適用於所有遵循現代規範的javascript,但本文中的所有程式碼都是針對Node.js 10、12和14版開發和測試的。
影片教學推薦:node js教學
看看這個小程式:
// 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 生成器這篇文章,那麼就應該知道,如果物件定義了Symbol.iterator
方法,並且該方法返回,則它在javascript 中是一個實現了迭代器協定的可迭代物件。當物件具有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
对象满足所有这些条件。
这就留下了一个问题——我们怎样才能遍历一个不可迭代但可以异步迭代的对象?
只用生成器的 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中的非同步生成器和非同步迭代的詳細內容。更多資訊請關注PHP中文網其他相關文章!