Detailed explanation of the new features of ES9: Async iteration

Detailed explanation of the new features of ES9: Async iteration

Asynchronous traversal

Before explaining asynchronous traversal, let's recall the synchronous traversal in ES6.

According to the definition of ES6, iteration mainly consists of three parts:

Iterable

First, let's look at the definition of Iterable:

interface Iterable {
    [Symbol.iterator]() : Iterator;
}

Iterable means that this object contains traversable data and needs to implement a factory method that can generate Iterator.

Iterator

interface Iterator {
    next() : IteratorResult;
}

Iterators can be constructed from Iterables. Iterator is a concept similar to a cursor, and the IteratorResult can be accessed through next.

IteratorResult

IteratorResult is the data obtained each time the next method is called.

interface IteratorResult {
    value: any;
    done: boolean;
}

In addition to a value representing the data to be obtained, IteratorResult also has a done, which indicates whether the traversal is completed.

Here is an example of iterating over an array:

> const iterable = ['a', 'b'];

> const iterator = iterable[Symbol.iterator]();

iterator.next()

{ value: 'a', done: false }

iterator.next()

{ value: 'b', done: false }

iterator.next()

{ value: undefined, done: true }

However, the above example traverses synchronous data. If we obtain asynchronous data, such as files downloaded from the http end, we want to traverse the file line by line. Because reading a row of data is an asynchronous operation, this involves traversing asynchronous data.

Add the asynchronous file reading method readLinesFromFile, then the synchronous traversal method is no longer applicable to asynchronous:

//No longer applicable for (const line of readLinesFromFile(fileName)) {
    console.log(line);
}

You may wonder, can we encapsulate the operation of asynchronously reading a line in a Promise and then traverse it in a synchronous way?

The idea is good, but in this case, it is impossible to detect whether the asynchronous operation has been completed. So this method is not feasible.

So ES9 introduced the concept of asynchronous traversal:

1. You can use Symbol.asyncIterator to get the iterator in asynchronous iterables.

2. The next() method of the asynchronous iterator returns a Promises object, which contains IteratorResults.

So, let's look at the API definition of asynchronous traversal:

interface AsyncIterable {
    [Symbol.asyncIterator]() : AsyncIterator;
}
interface AsyncIterator {
    next() : Promise<IteratorResult>;
}
interface IteratorResult {
    value: any;
    done: boolean;
}

Let's look at an asynchronous traversal application:

const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator.next()
.then(iterResult1 => {
    console.log(iterResult1); // { value: 'a', done: false }
    return asyncIterator.next();
})
.then(iterResult2 => {
    console.log(iterResult2); // { value: 'b', done: false }
    return asyncIterator.next();
})
.then(iterResult3 => {
    console.log(iterResult3); // { value: undefined, done: true }
});

Among them, createAsyncIterable will convert a synchronous iterable into an asynchronous iterable. We will see how it is generated in the following section.

Here we mainly focus on the traversal operation of asyncIterator.

Because ES8 introduces the Async operator, we can also rewrite the above code using the Async function:

async function f() {
    const asyncIterable = createAsyncIterable(['a', 'b']);
    const asyncIterator = asyncIterable[Symbol.asyncIterator]();
    console.log(await asyncIterator.next());
        // { value: 'a', done: false }
    console.log(await asyncIterator.next());
        // { value: 'b', done: false }
    console.log(await asyncIterator.next());
        // { value: undefined, done: true }
}

Asynchronous iterable traversal

Use for-of to traverse a synchronous iterable, and use for-await-of to traverse an asynchronous iterable.

async function f() {
    for await (const x of createAsyncIterable(['a', 'b'])) {
        console.log(x);
    }
}
// Output:
// a
// b

Note that await needs to be placed in an async function.

If an exception occurs in our asynchronous traversal, we can use try catch in for-await-of to catch the exception:

function createRejectingIterable() {
    return {
        [Symbol.asyncIterator]() {
            return this;
        },
        next() {
            return Promise.reject(new Error('Problem!'));
        },
    };
}
(async function () { 
    try {
        for await (const x of createRejectingIterable()) {
            console.log(x);
        }
    } catch (e) {
        console.error(e);
            // Error: Problem!
    }
})();

The synchronous iterable returns synchronous iterators, and the next method returns {value, done}.

If you use for-await-of, synchronous iterators will be converted into asynchronous iterators. The returned value is then converted into a Promise.

If the value returned by the synchronous next itself is a Promise object, the asynchronous return value is still the same promise.

That is to say, it will convert: Iterable<Promise<T>> into AsyncIterable<T>, as shown in the following example:

async function main() {
    const syncIterable = [
        Promise.resolve('a'),
        Promise.resolve('b'),
    ];
    for await (const x of syncIterable) {
        console.log(x);
    }
}
main();

// Output:
// a
// b

The above example converts a synchronous Promise into an asynchronous Promise.

async function main() {
    for await (const x of ['a', 'b']) {
        console.log(x);
    }
}
main();

// Output:
// c
// d

The example above converts a synchronous constant into a Promise. You can see that the results are the same.

Asynchronous iterable generation

Going back to the example above, we use createAsyncIterable(syncIterable) to convert syncIterable into AsyncIterable.

Let's see how this method is implemented:

async function* createAsyncIterable(syncIterable) {
    for (const elem of syncIterable) {
        yield elem;
    }
}

In the above code, we add async in front of a normal generator function, which means it is an asynchronous generator.

For ordinary generators, each time the next method is called, an object {value, done} is returned. This object is an encapsulation of the yield value.

For an asynchronous generator, each time the next method is called, a promise object containing object {value, done} is returned. This object is an encapsulation of the yield value.

Because a Promise object is returned, we do not need to wait for the result of the asynchronous execution to be completed before calling the next method again.

We can use a Promise.all to execute all asynchronous Promise operations at the same time:

const asyncGenObj = createAsyncIterable(['a', 'b']);
const [{value:v1},{value:v2}] = await Promise.all([
    asyncGenObj.next(), asyncGenObj.next()
]);
console.log(v1, v2); // ab

In createAsyncIterable, we create an asynchronous Iterable from a synchronous Iterable.

Next, let's look at how to create an asynchronous Iterable from an asynchronous Iterable.

From the previous section, we know that we can use for-await-of to read data from asynchronous Iterable, so we can use it like this:

async function* prefixLines(asyncIterable) {
    for await (const line of asyncIterable) {
        yield '> ' + line;
    }
}

In the generator article, we talked about calling generators within generators. That is, in a producer, another generator is called by using yield*.

Likewise, we can do the same thing in an asynchronous generator:

async function* gen1() {
    yield 'a';
    yield 'b';
    return 2;
}
async function* gen2() {
    const result = yield* gen1(); 
        // result === 2
}

(async function () {
    for await (const x of gen2()) {
        console.log(x);
    }
})();
// Output:
// a
// b

If an exception is thrown in an asynchronous generator, the exception will also be wrapped in a Promise:

async function* asyncGenerator() {
    throw new Error('Problem!');
}
asyncGenerator().next()
.catch(err => console.log(err)); // Error: Problem!

Async Methods and Async Generators

An asynchronous method is a method declared using async function, which returns a Promise object.

The return or thrown exception in the function will be used as the value in the returned Promise.

(async function () {
    return 'hello';
})()
.then(x => console.log(x)); // hello

(async function () {
    throw new Error('Problem!');
})()
.catch(x => console.error(x)); // Error: Problem!

Asynchronous generators are methods declared using async function *. It returns an asynchronous iterable.

By calling the next method of iterable, a Promise will be returned. The value yielded by the asynchronous generator is used to fill the value of the Promise. If an exception is thrown in the generator, it will also be caught by Promise.

async function* gen() {
    yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
    // { value: 'hello', done: false }

The above is a detailed explanation of the new feature of ES9, Async iteration. For more information about the new feature of ES9, Async iteration, please pay attention to other related articles on 123WORDPRESS.COM!

You may also be interested in:
  • ES6 Iterator traversal principle, application scenarios and related common knowledge expansion detailed explanation
  • ES6 New Features 2: Iterator (traversal) and for-of loop detailed explanation
  • Example of ES6 directory traversal function
  • Analysis of Iterator and for..of.. traversal usage in ES6
  • Detailed explanation of regular expression RegExp, a new feature of ES9
  • Understanding JavaScript Modifiers in ES7
  • Detailed explanation of the use of Async/await in ES7
  • Detailed explanation of how to use Await in ES7 to reduce callback nesting

<<:  MySQL max_allowed_packet setting

>>:  How to deal with garbled characters in Mysql database

Recommend

Tutorial diagram of using Jenkins for automated deployment under Windows

Today we will talk about how to use Jenkins+power...

Why do code standards require SQL statements not to have too many joins?

Free points Interviewer : Have you ever used Linu...

MySQL 5.7.11 zip installation and configuration method graphic tutorial

1. Download the MySQL 5.7.11 zip installation pac...

How to monitor the running status of docker container shell script

Scenario The company project is deployed in Docke...

When modifying a record in MySQL, the update operation field = field + string

In some scenarios, we need to modify our varchar ...

MySQL complete collapse: detailed explanation of query filter conditions

Overview In actual business scenario applications...

12 Javascript table controls (DataGrid) are sorted out

When the DataSource property of a DataGrid control...

Discussion on the problem of iframe node initialization

Today I suddenly thought of reviewing the producti...

JavaScript object built-in objects, value types and reference types explained

Table of contents Object Object Definition Iterat...

CSS Tutorial: CSS Attribute Media Type

One of the most important features of a style she...

Building command line applications with JavaScript

Table of contents 1. Install node 2. Install Comm...

Several ways to easily traverse object properties in JS

Table of contents 1. Self-enumerable properties 2...

Implementation of mysql decimal data type conversion

Recently, I encountered a database with the follo...