Skip to content

Commit 1369332

Browse files
committed
async generators
1 parent 2ee2751 commit 1369332

File tree

3 files changed

+322
-10
lines changed

3 files changed

+322
-10
lines changed

1-js/06-advanced-functions/13-generators/article.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ alert(sequence); // 0, 1, 2, 3
152152

153153
In the code above, `...generateSequence()` turns the iterable into array of items (read more about the spread operator in the chapter [](info:rest-parameters-spread-operator#spread-operator))
154154

155-
## Converting iterables to generators
155+
## Using generators instead of iterables
156156

157157
Some time ago, in the chapter [](info:iterable) we created an iterable `range` object that returns values `from..to`.
158158

@@ -203,6 +203,8 @@ alert(sequence); // 1, 2, 3, 4, 5
203203

204204
...But what if we'd like to keep a custom `range` object?
205205

206+
## Converting Symbol.iterator to generator
207+
206208
We can get the best from both worlds by providing a generator as `Symbol.iterator`:
207209

208210
```js run
@@ -220,7 +222,16 @@ let range = {
220222
alert( [...range] ); // 1,2,3,4,5
221223
```
222224

223-
The `range` object is now iterable. The last variant with a generator is much more concise than the original iterable code, and keeps the same functionality.
225+
The `range` object is now iterable.
226+
227+
That works pretty well, because when `range[Symbol.iterator]` is called:
228+
- it returns an object (now a generator)
229+
- that has `.next()` method (yep, a generator has it)
230+
- that returns values in the form `{value: ..., done: true/false}` (check, exactly what generator does).
231+
232+
That's not a coincidence, of course. Generators aim to make iterables easier, so we can see that.
233+
234+
The last variant with a generator is much more concise than the original iterable code, and keeps the same functionality.
224235

225236
```smart header="Generators may continue forever"
226237
In the examples above we generated finite sequences, but we can also make a generator that yields values forever. For instance, an unending sequence of pseudo-random numbers.
Lines changed: 285 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,297 @@
11

22
# Async iteration and generators
33

4-
In web-programming, we often need to work with fragmented data that comes piece-by-piece.
4+
Asynchronous iterators allow to iterate over data that comes asynchronously, on-demand.
55

6-
That happens when we upload or download a file. Or we need to fetch paginated data.
6+
For instance, when we download something chunk-by-chunk, or just expect a events to come asynchronously and would like to iterate over that -- async iterators and generators may come in handy.
77

8+
Let's see a simple example first, to grasp the syntax, and then build a real-life example.
89

9-
For
10-
Either we need to download or upload something, or we need
10+
## Async iterators
1111

12-
In the previous chapter we saw how `async/await` allows to write asynchronous code.
12+
Asynchronous iterators are totally similar to regular iterators, with a few syntactic differences.
1313

14-
But they don't solve
14+
Here's a small cheatsheet:
1515

16+
| | Iterators | Async iterators |
17+
|-------|-----------|-----------------|
18+
| Object method to provide iteraterable | `Symbol.iterator` | `Symbol.asyncIterator` |
19+
| `next()` return value is | any value | `Promise` |
20+
| to loop, use | `for..of` | `for await..of` |
1621

22+
Let's make an iterable `range` object, like we did in the chapter [](info:iterable), but now it will return values asynchronously, one per second:
1723

18-
Regular iterators work fine with the data that doesn't take time to generate.
24+
```js run
25+
let range = {
26+
from: 1,
27+
to: 5,
1928

20-
A `for..of` loop assumes that we
29+
// for await..of calls this method once in the very beginning
30+
*!*
31+
[Symbol.asyncIterator]() { // (1)
32+
*/!*
33+
// ...it returns the iterator object:
34+
// onward, for await..of works only with that object, asking it for next values
35+
return {
36+
current: this.from,
37+
last: this.to,
38+
39+
// next() is called on each iteration by the for..of loop
40+
*!*
41+
async next() { // (2)
42+
// it should return the value as an object {done:.., value :...}
43+
// (automatically wrapped into a promise by async)
44+
*/!*
45+
46+
// can use await inside, do async stuff:
47+
await new Promise(resolve => setTimeout(resolve, 1000)); // (4)
48+
49+
if (this.current <= this.last) {
50+
return { done: false, value: this.current++ };
51+
} else {
52+
return { done: true };
53+
}
54+
}
55+
};
56+
}
57+
};
58+
59+
(async () => {
60+
61+
*!*
62+
for await (let value of range) { // (3)
63+
alert(value); // 1,2,3,4,5
64+
}
65+
*/!*
66+
67+
})()
68+
```
69+
70+
As we can see, the components are similar to regular iterators:
71+
72+
1. To make an object asynchronously iterable, it must have a method `Symbol.asyncIterator` `(1)`.
73+
2. To iterate, we use `for await(let value of range)` `(3)`, namely add "await" after "for".
74+
3. It calls `range[Symbol.asyncIterator]` and expects it to return an object with `next` `(3)` method returning a promise.
75+
4. Then `next()` is called to obtain values. In that example values come with a delay of 1 second `(4)`.
76+
77+
````warn header="A spread operator doesn't work asynchronously"
78+
Features that require regular, synchronous iterators, don't work with asynchronous ones.
79+
80+
For instance, a spread operator won't work:
81+
```js
82+
alert( [...range] ); // Error, no Symbol.iterator
83+
```
84+
85+
That's natural, as it expects to find `Symbol.iterator`, and there's none.
86+
````
87+
88+
## Async generators
89+
90+
Javascript also provides generators, that are also iterable.
91+
92+
Let's recall a sequence generator from the chapter [](info:generators):
93+
94+
```js run
95+
function* generateSequence(start, end) {
96+
for (let i = start; i <= end; i++) {
97+
yield i;
98+
}
99+
}
100+
101+
for(let value of generateSequence(1, 5)) {
102+
alert(value); // 1, then 2, then 3, then 4, then 5
103+
}
104+
```
105+
106+
Normally, we can't use `await` in generators. But what if we need to?
107+
108+
No problem, let's make an async generator, like this:
109+
110+
```js run
111+
*!*async*/!* function* generateSequence(start, end) {
112+
113+
for (let i = start; i <= end; i++) {
114+
115+
*!*
116+
// yay, can use await!
117+
await new Promise(resolve => setTimeout(resolve, 1000));
118+
*/!*
119+
120+
yield i;
121+
}
122+
123+
}
124+
125+
(async () => {
126+
127+
let generator = generateSequence(1, 5);
128+
for *!*await*/!* (let value of generator) {
129+
alert(value); // 1, then 2, then 3, then 4, then 5
130+
}
131+
132+
})();
133+
```
134+
135+
So simple, a little bit magical. We add the `async` keyword, and the generator now can use `await` inside of it, rely on promises and other async functions.
136+
137+
Technically, the difference of such generator is that `generator.next()` method is now asynchronous.
138+
139+
Instead of `result = generator.next()` for a regular, non-async generator, values can be obtained like this:
140+
141+
```js
142+
result = await generator.next(); // result = {value: ..., done: true/false}
143+
```
144+
145+
## Iterables via async generators
146+
147+
When we'd like to make an object iterable, we should add `Symbol.iterator` to it. A common practice is to implement it via a generator, just because that's simple.
148+
149+
Let's recall an example from the chapter [](info:generators):
150+
151+
```js run
152+
let range = {
153+
from: 1,
154+
to: 5,
155+
156+
*[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
157+
for(let value = this.from; value <= this.to; value++) {
158+
yield value;
159+
}
160+
}
161+
};
162+
163+
for(let value of range) {
164+
alert(value); // 1, then 2, then 3, then 4, then 5
165+
}
166+
```
167+
168+
Here a custom object `range` was made iterable, the generator `*[Symbol.iterator]` implements the logic behind listing values.
169+
170+
We can make it iterable asynchronously by replacing `Symbol.iterator` with `Symbol.asyncIterator` and marking it `async`:
171+
172+
```js run
173+
let range = {
174+
from: 1,
175+
to: 5,
176+
177+
*!*
178+
async *[Symbol.asyncIterator]() { // same as [Symbol.asyncIterator]: async function*()
179+
*/!*
180+
for(let value = this.from; value <= this.to; value++) {
181+
182+
// make a pause between values, wait for something
183+
await new Promise(resolve => setTimeout(resolve, 1000));
184+
185+
yield value;
186+
}
187+
}
188+
};
189+
190+
(async () => {
191+
192+
for *!*await*/!* (let value of range) {
193+
alert(value); // 1, then 2, then 3, then 4, then 5
194+
}
195+
196+
})();
197+
```
198+
199+
## Real-life example
200+
201+
There are many online APIs that deliver paginated data. For instance, when we need a list of users, then we can fetch it page-by-page: a request returns a pre-defined count (e.g. 100 users), and provides an URL to the next page.
202+
203+
The pattern is very common, it's not about users, but just about anything. For instance, Github allows to retrieve commits in the same, paginated fasion:
204+
205+
- We should make a request to URL in the form `https://api.github.com/repos/(repo)/commits`.
206+
- It responds with a JSON of 30 commits, and also provides a link to the next page in the `Link` header.
207+
- Then we can use it for the next request, if need more commits, and so on.
208+
209+
What we'd like to have is an iterable source of commits, so that we could do it like this:
210+
211+
```js
212+
let repo = 'iliakan/javascript-tutorial-en'; // Github repository to get commits from
213+
214+
for await (let commit of fetchCommits(repo)) {
215+
// process commit
216+
}
217+
```
218+
219+
We'd like `fetchCommits` to get commits for us, making requests whenever needed. And let it care about all pagination stuff.
220+
221+
With async generators that's pretty easy to implement:
222+
223+
```js
224+
async function* fetchCommits(repo) {
225+
let url = `https://api.github.com/repos/${repo}/commits`;
226+
227+
while (url) {
228+
const response = await fetch(url, { // (1)
229+
headers: {'User-Agent': 'Our script'}, // github requires user-agent header
230+
});
231+
232+
const body = await response.json(); // (2) parses response as JSON (array of commits)
233+
234+
// (3) the URL of the next page is in the headers, extract it
235+
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
236+
nextPage = nextPage && nextPage[1];
237+
238+
url = nextPage;
239+
240+
for(let commit of body) { // (4) yield commits one by one, until the page ends
241+
yield commit;
242+
}
243+
}
244+
}
245+
```
246+
247+
1. We use the browser `fetch` method to download from a remote URL. It allows to supply authorization and other headers if needed (Github requires User-Agent).
248+
2. The result is parsed as JSON.
249+
3. ...And we extract 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 this: `https://api.github.com/repositories/93253246/commits?page=2`, it's generatd by Github itself.
250+
4. Then we yield all commits received, and when they finish -- the next `while(url)` iteration will trigger, making one more request.
251+
252+
An example of use (shows commit authors in console):
253+
254+
```js run
255+
(async () => {
256+
257+
let count = 0;
258+
259+
for await (const commit of fetchCommits('iliakan/javascript-tutorial-en')) {
260+
261+
console.log(commit.author.login);
262+
263+
if (++count == 100) { // let's stop at 100 commits
264+
break;
265+
}
266+
}
267+
268+
})();
269+
```
270+
271+
The internal mechanics is invisible from the outside. What we see -- is an async generator that returns commits, just what we need.
272+
273+
## Summary
274+
275+
Regular iterators and generators work fine with the data that doesn't take time to generate.
276+
277+
When we expect the data to come asynchronously, with delays, their async counterparts can be used, and `for await..of` instead of `for..of`.
278+
279+
Syntax differences between async and regular iterators:
280+
281+
| | Iterators | Async iterators |
282+
|-------|-----------|-----------------|
283+
| Object method to provide iteraterable | `Symbol.iterator` | `Symbol.asyncIterator` |
284+
| `next()` return value is | any value | `Promise` |
285+
286+
Syntax differences between async and regular generators:
287+
288+
| | Generators | Async generators |
289+
|-------|-----------|-----------------|
290+
| Declaration | `function*` | `async function*` |
291+
| `generator.next()` returns | `{value:…, done: true/false}` | `Promise` that resolves to `{value:…, done: true/false}` |
292+
293+
In web-development we often meet streams of data, when it flows chunk-by-chunk. For instance, downloading or uploading a big file.
294+
295+
We could use async generators to process such data, but there's also another API called Streams, that may be more convenient. It's not a part of Javascript language standard though.
296+
297+
Streams and async generators complement each other, both are great ways to handle async data flows.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<script>
2+
async function* fetchCommits(repo) {
3+
let url = `https://api.github.com/repos/${repo}/commits`;
4+
5+
while (url) {
6+
const response = await fetch(url, {
7+
headers: {'User-Agent': 'Our script'}, // github requires user-agent header
8+
});
9+
10+
const body = await response.json(); // parses response as JSON (array of commits)
11+
12+
// the URL of the next page is in the headers, extract it
13+
let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/);
14+
nextPage = nextPage && nextPage[1];
15+
16+
url = nextPage;
17+
18+
// yield commits one by one, when they finish - fetch a new page url
19+
for(let commit of body) {
20+
yield commit;
21+
}
22+
}
23+
}
24+
</script>

0 commit comments

Comments
 (0)