A deep dive into asynchronous generators and asynchronous iteration in Node.js

青灯夜游
Release: 2020-09-19 10:20:48
forward
2068 people have browsed it

A deep dive into asynchronous generators and asynchronous iteration in Node.js

The appearance of generator functions in JavaScript predates the introduction of async/await, which means that when creating an asynchronous generator (which always returns aPromiseand canawaitgenerator), it also introduces many things that need attention.

Today we’ll look at asynchronous generators and their close cousin, asynchronous iteration.

NOTE: Although these conceptsshouldapply to all javascript following modern specifications, all code in this article is specific to Node.js 10, 12, and 14 version developed and tested.

Video tutorial recommendation:node js tutorial

Asynchronous generator function

Look at this small program:

// 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()
Copy after login

This code defines a generator function, uses this function to create a generator object, and then usesfor ... ofto loop through the generator object. Pretty standard stuff - although you'd never actually use a generator for something as trivial as this. If you are not familiar with generators andfor ... ofloops, please read "Javascript Generators" and "ES6 Loops and Iterable Objects" These two articles. Before using async generators, you need to have a solid understanding of generators andfor ... ofloops.

Suppose we want to useawaitin the generator function. Node.js supports this feature as long as the function needs to be declared with theasynckeyword. If you are unfamiliar with asynchronous functions, please read the article "Writing Asynchronous Tasks in Modern JavaScript".

Modify the program below and useawaitin the generator.

// 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()
Copy after login

Also in actual work, you wouldn't do this - you mightawaita function from a third-party API or library. In order to make it easy for everyone to grasp, our examples are kept as simple as possible.

If you try to run the above program, you will encounter a problem:

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

JavaScript tells us that this generator is "non-iterable". At first glance, it seems that making a generator function asynchronous also means that the generator it produces is not iterable. This is a bit confusing since the purpose of generators is to produce "programmatically" iterable objects.

The next step is to figure out what happened.

Check the generator

If you read theJavascript generatorarticle, then you should know that if the object definesSymbol.iteratormethod, and the method returns, it is an iterable object that implements theiterator protocolin javascript. When an object has anextmethod, the object implements the iterator protocol, and thenextmethod returns a property with avalue,doneAn object with either one of the properties or both thevalueanddoneproperties.

If you use the following code to compare the generator objects returned by the asynchronous generator function and the regular generator function:

// 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()
Copy after login

, you will see that the formerdoes not haveSymbol.iteratormethod, while the latter has.

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

Both generator objectshaveanextmethod. If you modify the test code to call thisnextmethod:

// File: test-program.js /* ... */ const main = () => { const generator = createGenerator() const asyncGenerator = createAsyncGenerator() console.log('generator:',generator.next()) console.log('asyncGenerator',asyncGenerator.next()) } main()
Copy after login

, you will see another problem:

$ node test-program.js generator: { value: 'a', done: false } asyncGenerator Promise {  }
Copy after login

In order to make the object iterable,nextMethod needs to return an object withvalueanddoneproperties. Anasyncfunction will always return aPromiseobject. This feature will be carried over to generators created with asynchronous functions - these asynchronous generators will alwaysyieldaPromiseobject.

This behavior makes it impossible for generators ofasyncfunctions to implement the javascript iteration protocol.

Asynchronous Iteration

Fortunately there is a way to resolve this contradiction. If you take a look at the constructor or class

// File: test-program.js /* ... */ const main = () => { const generator = createGenerator() const asyncGenerator = createAsyncGenerator() console.log('asyncGenerator',asyncGenerator) }
Copy after login
returned by the

async

generator you can see that it is an object whose type or class or constructor isAsyncGeneratorwhile NotGenerator:

asyncGenerator Object [AsyncGenerator] {}
Copy after login

Although the object may not be iterable, it isasynchronous iterable.

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

但是,这不是最直接的循环机制。我既不喜欢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()
Copy after login

如果运行上述代码,则会看到异步生成器与可迭代对象已被成功循环,并且在循环体中得到了Promise的完全解析值。

$ node main.js a b c
Copy after login

这个for await ... of循环更喜欢实现了异步迭代器协议的对象。但是你可以用它遍历任何一种可迭代对象。

for await(const item of [1,2,3]) { console.log(item) }
Copy after login

当你使用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() + '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')
Copy after login

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

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

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

The above is the detailed content of A deep dive into asynchronous generators and asynchronous iteration in Node.js. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:segmentfault.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!