>  기사  >  웹 프론트엔드  >  Node.js의 비동기 생성기와 비동기 반복에 대한 심층 분석

Node.js의 비동기 생성기와 비동기 반복에 대한 심층 분석

青灯夜游
青灯夜游앞으로
2020-09-19 10:20:482079검색

Node.js의 비동기 생성기와 비동기 반복에 대한 심층 분석

JavaScript에 생성기 함수가 등장한 것은 async/await가 도입되기 이전입니다. 즉, 비동기 생성기를 생성할 때(항상 Promise를 반환하고 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

오늘은 비동기 생성기와 그 사촌격인 비동기 반복에 대해 살펴보겠습니다.

참고

: 이러한 개념은 최신 사양을 따르는 모든 자바스크립트에 적용되어야 하지만, 이 문서의 모든 코드는 Node.js 버전 10, 12, 14에서 개발 및 테스트되었습니다. 🎜
🎜추천 동영상 튜토리얼: node js 튜토리얼 a> 🎜

비동기 생성기 함수

🎜이 작은 프로그램을 보세요: 🎜
asyncGenerator Object [AsyncGenerator] {}
🎜이 코드는 생성기 객체를 생성하는 데 사용되는 생성기 함수를 정의합니다. 그런 다음 for ... of를 사용하여 생성기 개체를 반복합니다. 매우 표준적인 내용입니다. 비록 실제로 이렇게 사소한 일을 처리하기 위해 생성기를 사용하지는 않을 것입니다. 생성기와 for ... of 루프에 익숙하지 않은 경우 "Javascript Generator" 및 "ES6 루프 및 반복 가능한 객체" 이 두 기사입니다. 비동기 생성기를 사용하기 전에 생성기와 for ... of 루프에 대해 확실히 이해해야 합니다. 🎜🎜생성기 함수에서 await를 사용한다고 가정해 보겠습니다. Node.js는 함수가 async 키워드로 선언되어야 하는 한 이 기능을 지원합니다. 비동기 함수에 익숙하지 않은 경우 Writing Asynchronous in Modern JavaScript Mission" 기사입니다. 🎜🎜아래 프로그램을 수정하고 생성기에서 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()
🎜또한 실제 생활에서는 이렇게 하지 않을 것입니다. 아마도 제3자 API나 라이브러리의 함수를 기다릴 것입니다. 모든 사람이 쉽게 이해할 수 있도록 예제는 최대한 단순하게 유지됩니다. 🎜🎜위 프로그램을 실행하려고 하면 문제에 직면하게 됩니다. 🎜
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()
🎜JavaScript에서는 이 생성기가 "반복 불가능"하다고 알려줍니다. 언뜻 보면 생성기 함수를 비동기식으로 만드는 것은 생성하는 생성기가 반복 가능하지 않다는 것을 의미하는 것 같습니다. 제너레이터의 목적은 "프로그래밍 방식으로" 반복 가능한 객체를 생성하는 것이기 때문에 다소 혼란스럽습니다. 🎜🎜다음 단계는 무슨 일이 일어났는지 알아내는 것입니다. 🎜

생성기 확인

🎜
Javascript Generator 기사를 참조했다면 객체가 Symbol.iterator 메서드를 정의하고 메서드가 반환되면 이는 javascript에 있는 구현이라는 것을 알아야 합니다. Iterator 프로토콜 Iterable 객체 >. 객체에 next 메소드가 있는 경우 객체는 반복자 프로토콜을 구현하고 next 메소드는 value 속성으로 done을 반환합니다. 속성 중 하나이거나 valuedone 속성이 모두 있는 객체입니다. 🎜🎜다음 코드를 사용하여 비동기 생성기 함수와 일반 생성기 함수에 의해 반환된 생성기 개체를 비교하면 🎜
$ node main.js
a
b
c
🎜, 전자 🎜에는 🎜 Symbol.iterator가 없음을 알 수 있습니다. 방법을 사용하지만 후자는 사용합니다. 🎜
for await(const item of [1,2,3]) {
    console.log(item)
}
🎜두 생성기 개체 🎜 둘 다 🎜 next 메서드를 갖습니다. 이 next 메소드를 호출하도록 테스트 코드를 수정하는 경우: 🎜
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;)
🎜, 또 다른 문제가 발생합니다: 🎜rrreee🎜객체를 반복 가능하게 만들기 위해 next 메소드 valuedone 속성이 있는 객체를 반환해야 합니다. async 함수는 항상 Promise 객체를 반환합니다. 이 기능은 비동기 함수로 생성된 생성기로 확장됩니다. 이러한 비동기 생성기는 항상 Promise 개체를 생성합니다. 🎜🎜이러한 동작으로 인해 async 함수 생성기가 자바스크립트 반복 프로토콜을 구현할 수 없게 됩니다. 🎜

비동기 반복

🎜다행히도 이 모순을 해결할 수 있는 방법이 있습니다. async 생성기 🎜rrreee🎜에서 반환된 생성자 또는 클래스를 보면 해당 유형 또는 클래스 또는 생성자가 AsyncGenerator인 객체임을 알 수 있습니다. >생성기: 🎜rrreee🎜객체가 반복 가능하지 않더라도 🎜비동기적으로 반복 가능🎜합니다. 🎜

要想使对象能够异步迭代,它必须实现一个 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으로 문의하시기 바랍니다. 삭제