首頁  >  文章  >  web前端  >  深入研究Node.js中的非同步生成器和非同步迭代

深入研究Node.js中的非同步生成器和非同步迭代

青灯夜游
青灯夜游轉載
2020-09-19 10:20:482160瀏覽

深入研究Node.js中的非同步生成器和非同步迭代

生成器函數在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屬性之一或同時帶有valuedone 屬性的物件。

如果用下面這段程式碼比較非同步產生器函數與常規生成器函數傳回的生成器物件:

// 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 方法需要傳回帶有valuedone 屬性的物件。一個 async 函數將總是傳回一個 Promise 物件。這個特性會帶到用非同步函數建立的生成器上-這些非同步產生器總是會 yield 一個 Promise 物件。

這種行為使得 async 函數的生成器無法實作 javascript 迭代協定。

非同步迭代

幸運的是有辦法解決這個矛盾。如果看一看async 生成器傳回的建構子或類別

// File: test-program.js
/* ... */
const main = () => {
  const generator = createGenerator()
  const asyncGenerator = createAsyncGenerator()

  console.log(&#39;asyncGenerator&#39;,asyncGenerator)
}

可以看到它是一個對象,其型別或類別或建構子是AsyncGenerator 而不是Generator

asyncGenerator Object [AsyncGenerator] {}

儘管該物件有可能不是可迭代的,但它是非同步可迭代的。

要想使对象能够异步迭代,它必须实现一个 Symbol.asyncIterator 方法。这个方法必须返回一个对象,该对象实现了异步版本的迭代器协议。也就是说,对象必须具有返回 Promisenext 方法,并且这个 promise 必须最终解析为带有 donevalue 属性的对象。

一个 AsyncGenerator 对象满足所有这些条件。

这就留下了一个问题——我们怎样才能遍历一个不可迭代但可以异步迭代的对象?

for await … of 循环

只用生成器的 next 方法就可以手动迭代异步可迭代对象。 (注意,这里的 main 函数现在是 async main ——这样能够使我们在函数内部使用 await

// File: main.js
const createAsyncGenerator = async function*(){
  yield await new Promise((r) => r(&#39;a&#39;))
  yield &#39;b&#39;
  yield &#39;c&#39;
}

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(&#39;a&#39;))
  yield &#39;b&#39;
  yield &#39;c&#39;
}

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 解析为 donetrue 的对象,然后在 for await 循环之后继续执行代码。

下面的例子可以说明一点:

let count = 0
const getCount = () => {
  count++
  return `${count}. `
}

const createAsyncGenerator = async function*() {
  console.log(getCount() + &#39;entering createAsyncGenerator&#39;)

  console.log(getCount() + &#39;about to yield a&#39;)
  yield await new Promise((r)=>r(&#39;a&#39;))

  console.log(getCount() + &#39;re-entering createAsyncGenerator&#39;)
  console.log(getCount() + &#39;about to yield b&#39;)
  yield &#39;b&#39;

  console.log(getCount() + &#39;re-entering createAsyncGenerator&#39;)
  console.log(getCount() + &#39;about to yield c&#39;)
  yield &#39;c&#39;

  console.log(getCount() + &#39;re-entering createAsyncGenerator&#39;)
  console.log(getCount() + &#39;exiting createAsyncGenerator&#39;)
}

const main = async () => {
  console.log(getCount() + &#39;entering main&#39;)

  const asyncGenerator = createAsyncGenerator()
  console.log(getCount() + &#39;starting for await loop&#39;)
  for await(const item of asyncGenerator) {
    console.log(getCount() + &#39;entering for await loop&#39;)
    console.log(getCount() + item)
    console.log(getCount() + &#39;exiting for await loop&#39;)
  }
  console.log(getCount() + &#39;done with for await loop&#39;)
  console.log(getCount() + &#39;leaving main&#39;)
}

console.log(getCount() + &#39;before calling main&#39;)
main()
console.log(getCount() + &#39;after calling main&#39;)

这段代码你用了编号的日志记录语句,可让你跟踪其执行情况。作为练习,你需要自己运行程序然后查看执行结果是怎样的。

如果你不知道它的工作方式,就会使程序的执行产生混乱,但异步迭代的确是一项强大的技术。

更多编程相关知识,请访问:编程入门!!

以上是深入研究Node.js中的非同步生成器和非同步迭代的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除