Maison  >  Article  >  interface Web  >  Une plongée approfondie dans les générateurs asynchrones et l'itération asynchrone dans Node.js

Une plongée approfondie dans les générateurs asynchrones et l'itération asynchrone dans Node.js

青灯夜游
青灯夜游avant
2020-09-19 10:20:482160parcourir

Une plongée approfondie dans les générateurs asynchrones et l'itération asynchrone dans Node.js

L'apparition des fonctions génératrices en JavaScript est antérieure à l'introduction de async/await, ce qui signifie que lors de la création de générateurs asynchrones (générateurs qui renvoient toujours Promise et peuvent await) au en même temps, il introduit également de nombreuses questions qui nécessitent une attention particulière.

Aujourd'hui, nous allons examiner les générateurs asynchrones et leur proche cousin, l'itération asynchrone.

REMARQUE  : Bien que ces concepts devraient s'appliquer à tout javascript suivant les spécifications modernes, tout le code de cet article est spécifique aux versions Node.js 10, 12 et 14 développé et testé.

Recommandation de tutoriel vidéo : Tutoriel node js

Fonction de générateur asynchrone

Jetez un oeil à ce petit programme :

// 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()

Ce code définit une fonction génératrice, utilise cette fonction pour créer un objet générateur, puis utilise for ... of pour parcourir l'objet générateur. Des trucs assez standards - même si vous n'utiliseriez jamais un générateur pour quelque chose d'aussi trivial que celui-ci. Si vous n'êtes pas familier avec les générateurs et les for ... of boucles, veuillez lire les deux articles "Générateurs Javascript" et "Boucles ES6 et objets itérables". Avant d'utiliser des générateurs asynchrones, vous devez avoir une solide compréhension des générateurs et des boucles for ... of.

Supposons que nous voulions utiliser await dans une fonction génératrice, Node.js prend en charge cette fonctionnalité tant que la fonction doit être déclarée avec le mot-clé async. Si vous n'êtes pas familier avec les fonctions asynchrones, veuillez lire l'article "Écrire des tâches asynchrones en JavaScript moderne".

Modifiez le programme ci-dessous et utilisez await dans le générateur.

// 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()

De plus, dans la vraie vie, vous ne feriez pas cela - vous pourriez await fonctionner à partir d'une API ou d'une bibliothèque tierce. Afin de faciliter la compréhension de tous, nos exemples sont aussi simples que possible.

Si vous essayez d'exécuter le programme ci-dessus, vous rencontrerez un problème :

$ node main.js
/Users/alanstorm/Desktop/main.js:9
  for (const item of generator) {
                     ^
TypeError: generator is not iterable

JavaScript nous indique que ce générateur est "non itérable". À première vue, il semble que rendre asynchrone un générateur signifie également que le générateur qu’il produit n’est pas itérable. C'est un peu déroutant puisque le but des générateurs est de produire des objets itérables « par programmation ».

La prochaine étape consiste à découvrir ce qui s’est passé.

Vérifiez le générateur

Si vous lisez l'article Javascript Generator, alors vous devez savoir que si l'objet définit la méthode Symbol.iterator et que la méthode renvoie , alors c'est un objet itérable qui implémente le protocole Iterator en JavaScript. Lorsqu'un objet a une méthode next, l'objet implémente le protocole itérateur, et cette méthode next renvoie un objet avec l'attribut value, l'un des attributs done, ou les deux value et done attributs.

Si vous utilisez le code suivant pour comparer les objets générateurs renvoyés par la fonction générateur asynchrone et la fonction générateur régulière :

// 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()

, vous verrez que l'ancien n'a pas Symbol.iterator méthode, alors que cette dernière l'a.

$ node test-program.js
generator: [Function: [Symbol.iterator]]
asyncGenerator undefined

Les deux objets générateurs ont une next méthode. Si vous modifiez le code de test pour appeler cette méthode next :

// File: test-program.js

/* ... */

const main = () => {
  const generator = createGenerator()
  const asyncGenerator = createAsyncGenerator()

  console.log('generator:',generator.next())
  console.log('asyncGenerator',asyncGenerator.next())
}
main()

vous verrez un autre problème :

$ node test-program.js
generator: { value: 'a', done: false }
asyncGenerator Promise { <pending> }

Afin de rendre l'objet itérable, la méthode next doit renvoyer avec les attributs value et done. Une fonction async renverra toujours un objet Promise. Cette fonctionnalité s'applique aux générateurs créés avec des fonctions asynchrones - ces générateurs asynchrones seront toujours yield un Promise objet.

Ce comportement empêche les générateurs de async fonctions d'implémenter le protocole d'itération javascript.

Itération asynchrone

Heureusement, il existe des moyens de résoudre cette contradiction. Si vous regardez le constructeur ou la classe async

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

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

renvoyé par le générateur

vous pouvez voir qu'il s'agit d'un objet et que son type ou sa classe ou son constructeur est AsyncGenerator et non Generator :

asyncGenerator Object [AsyncGenerator] {}

Bien que l'objet ne soit pas itérable, il est itérable de manière asynchrone.

要想使对象能够异步迭代,它必须实现一个 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;)

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

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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer