minor fixes
This commit is contained in:
parent
88ad89f029
commit
ba5424a82d
1 changed files with 134 additions and 92 deletions
|
@ -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.
|
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
|
```js run
|
||||||
let range = {
|
let range = {
|
||||||
from: 1,
|
from: 1,
|
||||||
to: 5,
|
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 {
|
return {
|
||||||
current: this.from,
|
current: this.from,
|
||||||
last: this.to,
|
last: this.to,
|
||||||
|
|
||||||
// next() is called on each iteration by the for..of loop
|
|
||||||
*!*
|
*!*
|
||||||
next() { // (2)
|
next() { // called every iteration, to get the next value
|
||||||
// it should return the value as an object {done:.., value :...}
|
|
||||||
*/!*
|
*/!*
|
||||||
if (this.current <= this.last) {
|
if (this.current <= this.last) {
|
||||||
return { done: false, value: this.current++ };
|
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:
|
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
|
```js run
|
||||||
let range = {
|
let range = {
|
||||||
from: 1,
|
from: 1,
|
||||||
to: 5,
|
to: 5,
|
||||||
|
|
||||||
// for await..of calls this method once in the very beginning
|
|
||||||
*!*
|
*!*
|
||||||
[Symbol.asyncIterator]() { // (1)
|
[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 {
|
return {
|
||||||
current: this.from,
|
current: this.from,
|
||||||
last: this.to,
|
last: this.to,
|
||||||
|
|
||||||
// next() is called on each iteration by the for await..of loop
|
|
||||||
*!*
|
*!*
|
||||||
async next() { // (2)
|
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)
|
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)`.
|
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.
|
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 |
|
| | Iterators | Async iterators |
|
||||||
|-------|-----------|-----------------|
|
|-------|-----------|-----------------|
|
||||||
|
@ -128,14 +146,20 @@ For instance, a spread syntax won't work:
|
||||||
alert( [...range] ); // Error, no Symbol.iterator
|
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
|
```js run
|
||||||
function* generateSequence(start, end) {
|
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
|
```js run
|
||||||
*!*async*/!* function* generateSequence(start, end) {
|
*!*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++) {
|
for (let i = start; i <= end; i++) {
|
||||||
|
|
||||||
*!*
|
*!*
|
||||||
// yay, can use await!
|
// Wow, can use await!
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
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`.
|
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:
|
In a regular generator we'd use `result = generator.next()` to get values. In an async generator, we should add `await`, like this:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
result = await generator.next(); // result = {value: ..., done: true/false}
|
result = await generator.next(); // result = {value: ..., done: true/false}
|
||||||
```
|
```
|
||||||
|
That's why async generators work with `for await...of`.
|
||||||
|
````
|
||||||
|
|
||||||
## Async iterables
|
We can make the `range` object generate values asynchronously, once per second, by replacing synchronous `Symbol.iterator` with asynchronous `Symbol.asyncIterator`:
|
||||||
|
|
||||||
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`:
|
|
||||||
|
|
||||||
```js run
|
```js run
|
||||||
let range = {
|
let range = {
|
||||||
from: 1,
|
from: 1,
|
||||||
to: 5,
|
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++) {
|
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.
|
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.
|
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`.
|
- 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.
|
- 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.
|
- 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
|
```js
|
||||||
let repo = 'javascript-tutorial/en.javascript.info'; // GitHub repository to get commits from
|
for await (let commit of fetchCommits("username/repository")) {
|
||||||
|
|
||||||
for await (let commit of fetchCommits(repo)) {
|
|
||||||
// process commit
|
// 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`.
|
Here's such function, implemented as async generator:
|
||||||
|
|
||||||
With async generators that's pretty easy to implement:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
async function* fetchCommits(repo) {
|
async function* fetchCommits(repo) {
|
||||||
|
@ -294,7 +328,7 @@ async function* fetchCommits(repo) {
|
||||||
|
|
||||||
while (url) {
|
while (url) {
|
||||||
const response = await fetch(url, { // (1)
|
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)
|
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`.
|
More explanations about how it works:
|
||||||
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.
|
1. We use the browser [fetch](info:fetch) method to download the commits.
|
||||||
4. Then we yield all commits received, and when they finish, the next `while(url)` iteration will trigger, making one more request.
|
|
||||||
|
- 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):
|
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
|
## Summary
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue