Skip to content

Commit ba5424a

Browse files
committed
minor fixes
1 parent 88ad89f commit ba5424a

File tree

1 file changed

+132
-90
lines changed
  • 1-js/12-generators-iterators/2-async-iterators-generators

1 file changed

+132
-90
lines changed

1-js/12-generators-iterators/2-async-iterators-generators/article.md

Lines changed: 132 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,44 @@ Asynchronous iterators allow us to iterate over data that comes asynchronously,
55

66
Let's see a simple example first, to grasp the syntax, and then review a real-life use case.
77

8-
## Async iterators
8+
## Recall iterators
9+
10+
Let's recall the topic about iterators.
11+
12+
The idea is that we have an object, such as `range` here:
13+
```js
14+
let range = {
15+
from: 1,
16+
to: 5
17+
};
18+
```
19+
20+
...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.
21+
22+
In other words, we want to add an *iteration ability* to the object.
23+
24+
That can be implemented using a special method with the name `Symbol.iterator`:
925

10-
Asynchronous iterators are similar to regular iterators, with a few syntactic differences.
26+
- This method is called in the beginning of a `for..of` loop, and it should return an object with the `next` method.
27+
- For each iteration of `for..of`, the `next()` method is invoked for the next value.
28+
- The `next()` should return a value in the form `{done: true/false, value:<loop value>}`.
1129

12-
A "regular" iterable object, as described in the chapter <info:iterable>, looks like this:
30+
Here's an implementation for the `range`, with all the comments:
1331

1432
```js run
1533
let range = {
1634
from: 1,
1735
to: 5,
1836

19-
// for..of calls this method once in the very beginning
2037
*!*
21-
[Symbol.iterator]() {
38+
[Symbol.iterator]() { // called once, in the beginning of for..of
2239
*/!*
23-
// ...it returns the iterator object:
24-
// onward, for..of works only with that object,
25-
// asking it for next values using next()
2640
return {
2741
current: this.from,
2842
last: this.to,
2943

30-
// next() is called on each iteration by the for..of loop
3144
*!*
32-
next() { // (2)
33-
// it should return the value as an object {done:.., value :...}
45+
next() { // called every iteration, to get the next value
3446
*/!*
3547
if (this.current <= this.last) {
3648
return { done: false, value: this.current++ };
@@ -47,40 +59,46 @@ for(let value of range) {
4759
}
4860
```
4961

50-
If necessary, please refer to the [chapter about iterables](info:iterable) for details about regular iterators.
62+
If anything is unclear, please visit the [chapter about iterables](info:iterable), it gives all the details about regular iterators.
63+
64+
## Async iterators
65+
66+
Asynchronous iterators are similar to regular iterators. We also need to have an iterable object, but values are expected to come asynchronously.
67+
68+
The most common case is that the object needs to make a network request to deliver the next value.
69+
70+
Regular iterators, as the one above, require `next()` to return the next value right away. That's where asynchronous iterators come into play.
5171

5272
To make the object iterable asynchronously:
73+
5374
1. We need to use `Symbol.asyncIterator` instead of `Symbol.iterator`.
54-
2. `next()` should return a promise.
75+
2. The `next()` method should return a promise (to be fulfilled with the next value).
76+
- The `async` keyword handles it, we can simply make `async next()`.
5577
3. To iterate over such an object, we should use a `for await (let item of iterable)` loop.
78+
- Note the `await` word.
5679

57-
Let's make an iterable `range` object, like the one before, but now it will return values asynchronously, one per second:
80+
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.
81+
82+
All we need to do is to perform a few replacements in the code above:
5883

5984
```js run
6085
let range = {
6186
from: 1,
6287
to: 5,
6388

64-
// for await..of calls this method once in the very beginning
6589
*!*
6690
[Symbol.asyncIterator]() { // (1)
6791
*/!*
68-
// ...it returns the iterator object:
69-
// onward, for await..of works only with that object,
70-
// asking it for next values using next()
7192
return {
7293
current: this.from,
7394
last: this.to,
7495

75-
// next() is called on each iteration by the for await..of loop
7696
*!*
7797
async next() { // (2)
78-
// it should return the value as an object {done:.., value :...}
79-
// (automatically wrapped into a promise by async)
8098
*/!*
8199

82100
*!*
83-
// can use await inside, do async stuff:
101+
// note: we can use "await" inside the async next:
84102
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)
85103
*/!*
86104

@@ -112,7 +130,7 @@ As we can see, the structure is similar to regular iterators:
112130
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)`.
113131
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.
114132

115-
Here's a small cheatsheet:
133+
Here's a small table with the differences:
116134

117135
| | Iterators | Async iterators |
118136
|-------|-----------|-----------------|
@@ -128,14 +146,20 @@ For instance, a spread syntax won't work:
128146
alert( [...range] ); // Error, no Symbol.iterator
129147
```
130148

131-
That's natural, as it expects to find `Symbol.iterator`, same as `for..of` without `await`. Not `Symbol.asyncIterator`.
149+
That's natural, as it expects to find `Symbol.iterator`, not `Symbol.asyncIterator`.
150+
151+
It's also the case for `for..of`: the syntax without `await` needs `Symbol.iterator`.
132152
````
133153
134-
## Async generators
154+
## Recall generators
135155
136-
As we already know, JavaScript also supports generators, and they are iterable.
156+
Now let's recall generators. They are explained in detail in the chapter [](info:generators).
137157
138-
Let's recall a sequence generator from the chapter [](info:generators). It generates a sequence of values from `start` to `end`:
158+
For sheer simplicity, omitting some important stuff, they are "functions that generate (yield) values".
159+
160+
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.
161+
162+
This example generates a sequence of values from `start` to `end`:
139163
140164
```js run
141165
function* generateSequence(start, end) {
@@ -149,19 +173,58 @@ for(let value of generateSequence(1, 5)) {
149173
}
150174
```
151175
152-
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.
176+
As we already know, to make an object iterable, we should add `Symbol.iterator` to it.
153177
154-
But what if we need to use `await` in the generator body? To perform network requests, for instance.
178+
```js
179+
let range = {
180+
from: 1,
181+
to: 5,
182+
*!*
183+
[Symbol.iterator]() {
184+
return <object with next to make range iterable>
185+
}
186+
*/!*
187+
}
188+
```
155189
156-
No problem, just prepend it with `async`, like this:
190+
A common practice for `Symbol.iterator` is to return a generator, it makes the code shorter:
191+
192+
```js run
193+
let range = {
194+
from: 1,
195+
to: 5,
196+
197+
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
198+
for(let value = this.from; value <= this.to; value++) {
199+
yield value;
200+
}
201+
}
202+
};
203+
204+
for(let value of range) {
205+
alert(value); // 1, then 2, then 3, then 4, then 5
206+
}
207+
```
208+
209+
Please see the chapter [](info:generators) if you'd like more details.
210+
211+
Once again, what if we'd like to generate values asynchronously? From network requests, for instance.
212+
213+
In regular generators we can't use `await`. All values must come synchronously, as required by the `for..of` construct.
214+
215+
Let's switch to asynchronous generators, to make it possible.
216+
217+
## Async generators
218+
219+
To make an asynchronous generator, prepend `function*` with `async`, like this:
157220
158221
```js run
159222
*!*async*/!* function* generateSequence(start, end) {
160223
161224
for (let i = start; i <= end; i++) {
162225
163226
*!*
164-
// yay, can use await!
227+
// Wow, can use await!
165228
await new Promise(resolve => setTimeout(resolve, 1000));
166229
*/!*
167230
@@ -182,64 +245,31 @@ No problem, just prepend it with `async`, like this:
182245
183246
Now we have the async generator, iterable with `for await...of`.
184247
185-
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.
248+
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.
249+
250+
````smart header="Under-the-hood difference"
251+
Technically, if you're an advanced reader who remembers the details about generators, there's an internal difference.
186252
187-
Technically, another difference of an async generator is that its `generator.next()` method is now asynchronous also, it returns promises.
253+
For async generators, the `generator.next()` method is asynchronous, it returns promises.
188254
189255
In a regular generator we'd use `result = generator.next()` to get values. In an async generator, we should add `await`, like this:
190256
191257
```js
192258
result = await generator.next(); // result = {value: ..., done: true/false}
193259
```
260+
That's why async generators work with `for await...of`.
261+
````
194262

195-
## Async iterables
196-
197-
As we already know, to make an object iterable, we should add `Symbol.iterator` to it.
198-
199-
```js
200-
let range = {
201-
from: 1,
202-
to: 5,
203-
*!*
204-
[Symbol.iterator]() {
205-
return <object with next to make range iterable>
206-
}
207-
*/!*
208-
}
209-
```
210-
211-
A common practice for `Symbol.iterator` is to return a generator, rather than a plain object with `next` as in the example before.
212-
213-
Let's recall an example from the chapter [](info:generators):
214-
215-
```js run
216-
let range = {
217-
from: 1,
218-
to: 5,
219-
220-
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
221-
for(let value = this.from; value <= this.to; value++) {
222-
yield value;
223-
}
224-
}
225-
};
226-
227-
for(let value of range) {
228-
alert(value); // 1, then 2, then 3, then 4, then 5
229-
}
230-
```
231-
232-
Here a custom object `range` is iterable, and the generator `*[Symbol.iterator]` implements the logic for listing values.
233-
234-
If we'd like to add async actions into the generator, then we should replace `Symbol.iterator` with async `Symbol.asyncIterator`:
263+
We can make the `range` object generate values asynchronously, once per second, by replacing synchronous `Symbol.iterator` with asynchronous `Symbol.asyncIterator`:
235264

236265
```js run
237266
let range = {
238267
from: 1,
239268
to: 5,
240269

270+
// this line is same as [Symbol.asyncIterator]: async function*() {
241271
*!*
242-
async *[Symbol.asyncIterator]() { // same as [Symbol.asyncIterator]: async function*()
272+
async *[Symbol.asyncIterator]() {
243273
*/!*
244274
for(let value = this.from; value <= this.to; value++) {
245275

@@ -262,39 +292,43 @@ let range = {
262292

263293
Now values come with a delay of 1 second between them.
264294

265-
## Real-life example
295+
So, we can make any object asynchronously iterable by adding an async generator as its `Symbol.asyncIterator` method, and letting it to generate values.
266296

267-
So far we've seen simple examples, to gain basic understanding. Now let's review a real-life use case.
297+
## Real-life example: paginated data
298+
299+
So far we've seen basic examples, to gain understanding. Now let's review a real-life use case.
268300

269301
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.
270302

271-
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:
303+
This pattern is very common. It's not about users, but just about anything.
304+
305+
For instance, GitHub allows us to retrieve commits in the same, paginated fashion:
272306

273307
- We should make a request to `fetch` in the form `https://api.github.com/repos/<repo>/commits`.
274308
- It responds with a JSON of 30 commits, and also provides a link to the next page in the `Link` header.
275309
- Then we can use that link for the next request, to get more commits, and so on.
276310

277-
But we'd like to have a simpler API: an iterable object with commits, so that we could go over them like this:
311+
For our code, we'd like to have a simpler way to get commits.
278312

279-
```js
280-
let repo = 'javascript-tutorial/en.javascript.info'; // GitHub repository to get commits from
313+
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`.
281314

282-
for await (let commit of fetchCommits(repo)) {
315+
So the usage will be like this:
316+
317+
```js
318+
for await (let commit of fetchCommits("username/repository")) {
283319
// process commit
284320
}
285321
```
286322

287-
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`.
288-
289-
With async generators that's pretty easy to implement:
323+
Here's such function, implemented as async generator:
290324

291325
```js
292326
async function* fetchCommits(repo) {
293327
let url = `https://api.github.com/repos/${repo}/commits`;
294328

295329
while (url) {
296330
const response = await fetch(url, { // (1)
297-
headers: {'User-Agent': 'Our script'}, // github requires user-agent header
331+
headers: {'User-Agent': 'Our script'}, // github needs any user-agent header
298332
});
299333

300334
const body = await response.json(); // (2) response is JSON (array of commits)
@@ -312,10 +346,16 @@ async function* fetchCommits(repo) {
312346
}
313347
```
314348
315-
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`.
316-
2. The fetch result is parsed as JSON. That's again a `fetch`-specific method.
317-
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.
318-
4. Then we yield all commits received, and when they finish, the next `while(url)` iteration will trigger, making one more request.
349+
More explanations about how it works:
350+
351+
1. We use the browser [fetch](info:fetch) method to download the commits.
352+
353+
- The initial URL is `https://api.github.com/repos/<repo>/commits`, and the next page will be in the `Link` header of the response.
354+
- The `fetch` method allows us to supply authorization and other headers if needed -- here GitHub requires `User-Agent`.
355+
2. The commits are returned in JSON format.
356+
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.
357+
- The next page URL may look like `https://api.github.com/repositories/93253246/commits?page=2`. It's generated by GitHub itself.
358+
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.
319359
320360
An example of use (shows commit authors in console):
321361
@@ -336,7 +376,9 @@ An example of use (shows commit authors in console):
336376
})();
337377
```
338378

339-
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.
379+
That's just what we wanted.
380+
381+
The internal mechanics of paginated requests is invisible from the outside. For us it's just an async generator that returns commits.
340382

341383
## Summary
342384

0 commit comments

Comments
 (0)