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.
|
||||
|
||||
## 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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue