⬆️ ⬇️

New JavaScript: Asynchronous Iterators

In this small post I want to talk about one interesting proposal (English proposal) in the standard EcmaScript. It will be about asynchronous iterators, about what it is, how to use them, and why they are needed by a simple developer.



Asynchronous iterators are an extension of the capabilities of ordinary iterators, which, using a for-of / for-await-of loop, allow you to run through all elements of a collection.



For a start, it’s worth explaining what I mean by generators, and what by iterators, since I will often use these terms. A generator is a function that returns an iterator, and an iterator is an object containing the next() method, which in turn returns the next value.



Example
 function* generator () { //   yield 1 } const iterator = generator() //     console.log(iterator.next()) ///  { value: 1, done: false } 


I would like to dwell on iterators in some detail and explain their meaning at present. Modern JavaScript (standard ES6 / ES7) allows you to iterate over the values ​​of a collection (for example, Array , Set , Map , etc.) alternately, without unnecessary fuss with indexes. For this, an iterator protocol was adopted, which is defined in the prototype of the collection using the Symbol.iterator :



 //  ,    //   Range function Range (start, stop) { this.start = start this.stop = stop } //  ,     //      ,       for-of Range.prototype[Symbol.iterator] = function *values () { for (let i = this.start; i < this.stop; i++) { yield i } } //    const range = new Range(1, 5) //        [Symbol.iterator]() //      for (let number of range) { console.log(number) // 1, 2, 3, 4 } 


Each iterator (in our case, this range[Symbol.iterator]() ) has a next() method that returns an object containing 2 fields: value and done , containing the current value and a flag indicating the end of the generator, respectively. This object can be described with the following interface:



 interface IteratorResult<T> { value: T; done: Boolean; } 


More details about the generators can be read on MDN .



A little explanation

By the way, if we already have an iterator and we want to walk through it using for-of , then we do not need to bring it back to our (or any other iterated) type, since each iterator has the same [Symbol.iterator] method that returns this :



 const iter = range[Symbol.iterator]() assert.strictEqual(iter, iter[Symbol.iterator]()) 


I hope everything is clear here. Now a little more to say about asynchronous functions.



In ES7, the async / await syntax was proposed. In fact, it is sugar that allows you to work with Promise in a pseudo-sync style:



 async function request (url) { const response = await fetch(url) return await response.json() } //  function request (url) { return fetch(url) .then(response => response.json()) } 


The difference from the usual function is that the async function always returns a Promise, even if we do the usual return 1 , we get a Promise , which when resolved returns 1 .



Great, now finally go to asynchronous iterators.



Following the asynchronous functions ( async function () { ... } ), asynchronous iterators were proposed that can be used inside these same functions:



 async function* createQueue () { yield 1 yield 2 // ... } async function handle (queue) { for await (let value of queue) { console.log(value) // 1, 2, ... } } 


At the moment, asynchronous iterators are in sentences , in the 3rd stage (candidate), which means that the syntax is stabilized and is waiting to be included in the standard. This proposal has not yet been implemented in any JavaScript engine, but you can still try and play with it using the babel-plugin-transform-async-generator-functions Babel plugin:



package.json
 { "dependencies": { "babel-preset-es2015-node": "···", "babel-preset-es2016": "···", "babel-preset-es2017": "···", "babel-plugin-transform-async-generator-functions": "···" // ··· }, "babel": { "presets": [ "es2015-node", "es2016", "es2017" ], "plugins": [ "transform-async-generator-functions" ] }, // ··· } 


taken from the blog 2ality , the full code with examples of use can be viewed in rauschma / async-iter-demo



So, how do asynchronous iterators differ from ordinary ones? As mentioned above, the iterator returns an IteratorResult value. The asynchronous iterator always returns a Promise<IteratorResult> . This means that in order to get the value and understand, you need to continue executing the cycle or not, you need to wait for the permission (resolve) of the promise, which will return the IteratorResult . That is why a new for-await-of syntax was introduced, which does all this work.



A legitimate question arises: why introduce a new syntax, why not return IteratorResult<Promise> , not Promise<IteratorResult> and wait ( await ... ) with his hands (I apologize for this strange expression)? This is done for those cases where we cannot determine from the inside of a synchronous generator whether there is a next value or not. For example, you need to go to a certain remote queue on the network and pick up the next value, if the queue is empty, then exit the cycle.



Well, we figured it out, the last question remains - the use of asynchronous generators and iterators. Everything is quite simple here: add the async keyword to the generator and we get an asynchronous generator:



 //    async function* queue () { //       while (true) { //   const task = await redis.lpop('tasks-queue') if (task === null) { //   ,      //    ,    Promise<IteratorResult> return } else { //   yield task } } } //     async function handle () { //     const tasks = queue() //      for await (const task of tasks) { //   console.log(task) } } 


If we want our own structure to be asynchronously iterated using for-await-of , then we need to implement the [Symbol.asyncIterator] method:



 function MyQueue (name) { this.name = name } MyQueue.prototype[Symbol.asyncIterator] = async function* values () { //   ,      while (true) { const task = await redis.lpop(this.name) if (task === null) { return } else { yield task } } } async function handle () { const tasks = new MyQueue('tasks-queue') for await (const task of tasks) { console.log(task) } } 


That's all. I hope this article was interesting and at least to some extent useful. Thanks for attention.



Links





')

Source: https://habr.com/ru/post/324004/



All Articles