minor fixes

This commit is contained in:
Ilya Kantor 2020-09-05 18:04:40 +03:00
parent 88ad89f029
commit ba5424a82d

View file

@ -5,32 +5,44 @@ Asynchronous iterators allow us to iterate over data that comes asynchronously,
Let's see a simple example first, to grasp the syntax, and then review a real-life use case.
## Async iterators
## Recall iterators
Asynchronous iterators are similar to regular iterators, with a few syntactic differences.
Let's recall the topic about iterators.
A "regular" iterable object, as described in the chapter <info:iterable>, looks like this:
The idea is that we have an object, such as `range` here:
```js
let range = {
from: 1,
to: 5
};
```
...And we'd like to use `for..of` loop on it, such as `for(value of range)`, to get values from `1` to `5`. And otherwise use the object, as if it were an array.
In other words, we want to add an *iteration ability* to the object.
That can be implemented using a special method with the name `Symbol.iterator`:
- This method is called in the beginning of a `for..of` loop, and it should return an object with the `next` method.
- For each iteration of `for..of`, the `next()` method is invoked for the next value.
- The `next()` should return a value in the form `{done: true/false, value:<loop value>}`.
Here's an implementation for the `range`, with all the comments:
```js run
let range = {
from: 1,
to: 5,
// for..of calls this method once in the very beginning
*!*
[Symbol.iterator]() {
[Symbol.iterator]() { // called once, in the beginning of for..of
*/!*
// ...it returns the iterator object:
// onward, for..of works only with that object,
// asking it for next values using next()
return {
current: this.from,
last: this.to,
// next() is called on each iteration by the for..of loop
*!*
next() { // (2)
// it should return the value as an object {done:.., value :...}
next() { // called every iteration, to get the next value
*/!*
if (this.current <= this.last) {
return { done: false, value: this.current++ };
@ -47,40 +59,46 @@ for(let value of range) {
}
```
If necessary, please refer to the [chapter about iterables](info:iterable) for details about regular iterators.
If anything is unclear, please visit the [chapter about iterables](info:iterable), it gives all the details about regular iterators.
## Async iterators
Asynchronous iterators are similar to regular iterators. We also need to have an iterable object, but values are expected to come asynchronously.
The most common case is that the object needs to make a network request to deliver the next value.
Regular iterators, as the one above, require `next()` to return the next value right away. That's where asynchronous iterators come into play.
To make the object iterable asynchronously:
1. We need to use `Symbol.asyncIterator` instead of `Symbol.iterator`.
2. `next()` should return a promise.
3. To iterate over such an object, we should use a `for await (let item of iterable)` loop.
Let's make an iterable `range` object, like the one before, but now it will return values asynchronously, one per second:
1. We need to use `Symbol.asyncIterator` instead of `Symbol.iterator`.
2. The `next()` method should return a promise (to be fulfilled with the next value).
- The `async` keyword handles it, we can simply make `async next()`.
3. To iterate over such an object, we should use a `for await (let item of iterable)` loop.
- Note the `await` word.
As a starting example, let's make an iterable `range` object, similar like the one before, but now it will return values asynchronously, one per second.
All we need to do is to perform a few replacements in the code above:
```js run
let range = {
from: 1,
to: 5,
// for await..of calls this method once in the very beginning
*!*
[Symbol.asyncIterator]() { // (1)
*/!*
// ...it returns the iterator object:
// onward, for await..of works only with that object,
// asking it for next values using next()
return {
current: this.from,
last: this.to,
// next() is called on each iteration by the for await..of loop
*!*
async next() { // (2)
// it should return the value as an object {done:.., value :...}
// (automatically wrapped into a promise by async)
*/!*
*!*
// can use await inside, do async stuff:
// note: we can use "await" inside the async next:
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
*/!*
@ -112,7 +130,7 @@ As we can see, the structure is similar to regular iterators:
3. The `next()` method doesn't have to be `async`, it may be a regular method returning a promise, but `async` allows us to use `await`, so that's convenient. Here we just delay for a second `(3)`.
4. To iterate, we use `for await(let value of range)` `(4)`, namely add "await" after "for". It calls `range[Symbol.asyncIterator]()` once, and then its `next()` for values.
Here's a small cheatsheet:
Here's a small table with the differences:
| | Iterators | Async iterators |
|-------|-----------|-----------------|
@ -128,14 +146,20 @@ For instance, a spread syntax won't work:
alert( [...range] ); // Error, no Symbol.iterator
```
That's natural, as it expects to find `Symbol.iterator`, same as `for..of` without `await`. Not `Symbol.asyncIterator`.
That's natural, as it expects to find `Symbol.iterator`, not `Symbol.asyncIterator`.
It's also the case for `for..of`: the syntax without `await` needs `Symbol.iterator`.
````
## Async generators
## Recall generators
As we already know, JavaScript also supports generators, and they are iterable.
Now let's recall generators. They are explained in detail in the chapter [](info:generators).
Let's recall a sequence generator from the chapter [](info:generators). It generates a sequence of values from `start` to `end`:
For sheer simplicity, omitting some important stuff, they are "functions that generate (yield) values".
Generators are labelled with `function*` (note the start) and use `yield` to generate a value, then we can use `for..of` to loop over them.
This example generates a sequence of values from `start` to `end`:
```js run
function* generateSequence(start, end) {
@ -149,11 +173,50 @@ for(let value of generateSequence(1, 5)) {
}
```
In regular generators we can't use `await`. All values must come synchronously: there's no place for delay in `for..of`, it's a synchronous construct.
As we already know, to make an object iterable, we should add `Symbol.iterator` to it.
But what if we need to use `await` in the generator body? To perform network requests, for instance.
```js
let range = {
from: 1,
to: 5,
*!*
[Symbol.iterator]() {
return <object with next to make range iterable>
}
*/!*
}
```
No problem, just prepend it with `async`, like this:
A common practice for `Symbol.iterator` is to return a generator, it makes the code shorter:
```js run
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
for(let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
```
Please see the chapter [](info:generators) if you'd like more details.
Once again, what if we'd like to generate values asynchronously? From network requests, for instance.
In regular generators we can't use `await`. All values must come synchronously, as required by the `for..of` construct.
Let's switch to asynchronous generators, to make it possible.
## Async generators
To make an asynchronous generator, prepend `function*` with `async`, like this:
```js run
*!*async*/!* function* generateSequence(start, end) {
@ -161,7 +224,7 @@ No problem, just prepend it with `async`, like this:
for (let i = start; i <= end; i++) {
*!*
// yay, can use await!
// Wow, can use await!
await new Promise(resolve => setTimeout(resolve, 1000));
*/!*
@ -182,64 +245,31 @@ No problem, just prepend it with `async`, like this:
Now we have the async generator, iterable with `for await...of`.
It's indeed very simple. We add the `async` keyword, and the generator now can use `await` inside of it, rely on promises and other async functions.
It's really simple. We add the `async` keyword, and the generator now can use `await` inside of it, rely on promises and other async functions.
Technically, another difference of an async generator is that its `generator.next()` method is now asynchronous also, it returns promises.
````smart header="Under-the-hood difference"
Technically, if you're an advanced reader who remembers the details about generators, there's an internal difference.
For async generators, the `generator.next()` method is asynchronous, it returns promises.
In a regular generator we'd use `result = generator.next()` to get values. In an async generator, we should add `await`, like this:
```js
result = await generator.next(); // result = {value: ..., done: true/false}
```
That's why async generators work with `for await...of`.
````
## Async iterables
As we already know, to make an object iterable, we should add `Symbol.iterator` to it.
```js
let range = {
from: 1,
to: 5,
*!*
[Symbol.iterator]() {
return <object with next to make range iterable>
}
*/!*
}
```
A common practice for `Symbol.iterator` is to return a generator, rather than a plain object with `next` as in the example before.
Let's recall an example from the chapter [](info:generators):
```js run
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
for(let value of range) {
alert(value); // 1, then 2, then 3, then 4, then 5
}
```
Here a custom object `range` is iterable, and the generator `*[Symbol.iterator]` implements the logic for listing values.
If we'd like to add async actions into the generator, then we should replace `Symbol.iterator` with async `Symbol.asyncIterator`:
We can make the `range` object generate values asynchronously, once per second, by replacing synchronous `Symbol.iterator` with asynchronous `Symbol.asyncIterator`:
```js run
let range = {
from: 1,
to: 5,
// this line is same as [Symbol.asyncIterator]: async function*() {
*!*
async *[Symbol.asyncIterator]() { // same as [Symbol.asyncIterator]: async function*()
async *[Symbol.asyncIterator]() {
*/!*
for(let value = this.from; value <= this.to; value++) {
@ -262,31 +292,35 @@ let range = {
Now values come with a delay of 1 second between them.
## Real-life example
So, we can make any object asynchronously iterable by adding an async generator as its `Symbol.asyncIterator` method, and letting it to generate values.
So far we've seen simple examples, to gain basic understanding. Now let's review a real-life use case.
## Real-life example: paginated data
So far we've seen basic examples, to gain understanding. Now let's review a real-life use case.
There are many online services that deliver paginated data. For instance, when we need a list of users, a request returns a pre-defined count (e.g. 100 users) - "one page", and provides a URL to the next page.
This pattern is very common. It's not about users, but just about anything. For instance, GitHub allows us to retrieve commits in the same, paginated fashion:
This pattern is very common. It's not about users, but just about anything.
For instance, GitHub allows us to retrieve commits in the same, paginated fashion:
- We should make a request to `fetch` in the form `https://api.github.com/repos/<repo>/commits`.
- It responds with a JSON of 30 commits, and also provides a link to the next page in the `Link` header.
- Then we can use that link for the next request, to get more commits, and so on.
But we'd like to have a simpler API: an iterable object with commits, so that we could go over them like this:
For our code, we'd like to have a simpler way to get commits.
Let's make a function `fetchCommits(repo)` that gets commits for us, making requests whenever needed. And let it care about all pagination stuff. For us it'll be a simple async iteration `for await..of`.
So the usage will be like this:
```js
let repo = 'javascript-tutorial/en.javascript.info'; // GitHub repository to get commits from
for await (let commit of fetchCommits(repo)) {
for await (let commit of fetchCommits("username/repository")) {
// process commit
}
```
We'd like to make a function `fetchCommits(repo)` that gets commits for us, making requests whenever needed. And let it care about all pagination stuff. For us it'll be a simple `for await..of`.
With async generators that's pretty easy to implement:
Here's such function, implemented as async generator:
```js
async function* fetchCommits(repo) {
@ -294,7 +328,7 @@ async function* fetchCommits(repo) {
while (url) {
const response = await fetch(url, { // (1)
headers: {'User-Agent': 'Our script'}, // github requires user-agent header
headers: {'User-Agent': 'Our script'}, // github needs any user-agent header
});
const body = await response.json(); // (2) response is JSON (array of commits)
@ -312,10 +346,16 @@ async function* fetchCommits(repo) {
}
```
1. We use the browser [fetch](info:fetch) method to download from a remote URL. It allows us to supply authorization and other headers if needed -- here GitHub requires `User-Agent`.
2. The fetch result is parsed as JSON. That's again a `fetch`-specific method.
3. We should get the next page URL from the `Link` header of the response. It has a special format, so we use a regexp for that. The next page URL may look like `https://api.github.com/repositories/93253246/commits?page=2`. It's generated by GitHub itself.
4. Then we yield all commits received, and when they finish, the next `while(url)` iteration will trigger, making one more request.
More explanations about how it works:
1. We use the browser [fetch](info:fetch) method to download the commits.
- The initial URL is `https://api.github.com/repos/<repo>/commits`, and the next page will be in the `Link` header of the response.
- The `fetch` method allows us to supply authorization and other headers if needed -- here GitHub requires `User-Agent`.
2. The commits are returned in JSON format.
3. We should get the next page URL from the `Link` header of the response. It has a special format, so we use a regular expression for that.
- The next page URL may look like `https://api.github.com/repositories/93253246/commits?page=2`. It's generated by GitHub itself.
4. Then we yield the received commits one by one, and when they finish, the next `while(url)` iteration will trigger, making one more request.
An example of use (shows commit authors in console):
@ -336,7 +376,9 @@ An example of use (shows commit authors in console):
})();
```
That's just what we wanted. The internal mechanics of paginated requests is invisible from the outside. For us it's just an async generator that returns commits.
That's just what we wanted.
The internal mechanics of paginated requests is invisible from the outside. For us it's just an async generator that returns commits.
## Summary