You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
@@ -5,32 +5,44 @@ Asynchronous iterators allow us to iterate over data that comes asynchronously,
5
5
6
6
Let's see a simple example first, to grasp the syntax, and then review a real-life use case.
7
7
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`:
9
25
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>}`.
11
29
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:
13
31
14
32
```js run
15
33
let range = {
16
34
from:1,
17
35
to:5,
18
36
19
-
// for..of calls this method once in the very beginning
20
37
*!*
21
-
[Symbol.iterator]() {
38
+
[Symbol.iterator]() {// called once, in the beginning of for..of
22
39
*/!*
23
-
// ...it returns the iterator object:
24
-
// onward, for..of works only with that object,
25
-
// asking it for next values using next()
26
40
return {
27
41
current:this.from,
28
42
last:this.to,
29
43
30
-
// next() is called on each iteration by the for..of loop
31
44
*!*
32
-
next() { // (2)
33
-
// it should return the value as an object {done:.., value :...}
45
+
next() { // called every iteration, to get the next value
34
46
*/!*
35
47
if (this.current<=this.last) {
36
48
return { done:false, value:this.current++ };
@@ -47,40 +59,46 @@ for(let value of range) {
47
59
}
48
60
```
49
61
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.
51
71
52
72
To make the object iterable asynchronously:
73
+
53
74
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()`.
55
77
3. To iterate over such an object, we should use a `for await (let item of iterable)` loop.
78
+
- Note the `await` word.
56
79
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:
58
83
59
84
```js run
60
85
let range = {
61
86
from:1,
62
87
to:5,
63
88
64
-
// for await..of calls this method once in the very beginning
65
89
*!*
66
90
[Symbol.asyncIterator]() { // (1)
67
91
*/!*
68
-
// ...it returns the iterator object:
69
-
// onward, for await..of works only with that object,
70
-
// asking it for next values using next()
71
92
return {
72
93
current:this.from,
73
94
last:this.to,
74
95
75
-
// next() is called on each iteration by the for await..of loop
76
96
*!*
77
97
asyncnext() { // (2)
78
-
// it should return the value as an object {done:.., value :...}
79
-
// (automatically wrapped into a promise by async)
@@ -112,7 +130,7 @@ As we can see, the structure is similar to regular iterators:
112
130
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)`.
113
131
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.
114
132
115
-
Here's a small cheatsheet:
133
+
Here's a small table with the differences:
116
134
117
135
|| Iterators | Async iterators |
118
136
|-------|-----------|-----------------|
@@ -128,14 +146,20 @@ For instance, a spread syntax won't work:
128
146
alert( [...range] ); // Error, no Symbol.iterator
129
147
```
130
148
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`.
132
152
````
133
153
134
-
## Async generators
154
+
## Recall generators
135
155
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).
137
157
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`:
139
163
140
164
```js run
141
165
function* generateSequence(start, end) {
@@ -149,19 +173,58 @@ for(let value of generateSequence(1, 5)) {
149
173
}
150
174
```
151
175
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.
153
177
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
+
```
155
189
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:
await new Promise(resolve => setTimeout(resolve, 1000));
166
229
*/!*
167
230
@@ -182,64 +245,31 @@ No problem, just prepend it with `async`, like this:
182
245
183
246
Now we have the async generator, iterable with `for await...of`.
184
247
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.
186
252
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.
188
254
189
255
In a regular generator we'd use `result = generator.next()` to get values. In an async generator, we should add `await`, like this:
190
256
191
257
```js
192
258
result = await generator.next(); // result = {value: ..., done: true/false}
193
259
```
260
+
That's why async generators work with `for await...of`.
261
+
````
194
262
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`:
235
264
236
265
```js run
237
266
let range = {
238
267
from:1,
239
268
to:5,
240
269
270
+
// this line is same as [Symbol.asyncIterator]: async function*() {
241
271
*!*
242
-
async *[Symbol.asyncIterator]() { // same as [Symbol.asyncIterator]: async function*()
272
+
async*[Symbol.asyncIterator]() {
243
273
*/!*
244
274
for(let value =this.from; value <=this.to; value++) {
245
275
@@ -262,39 +292,43 @@ let range = {
262
292
263
293
Now values come with a delay of 1 second between them.
264
294
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.
266
296
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.
268
300
269
301
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.
270
302
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:
272
306
273
307
- We should make a request to `fetch` in the form `https://api.github.com/repos/<repo>/commits`.
274
308
- It responds with a JSON of 30 commits, and also provides a link to the next page in the `Link` header.
275
309
- Then we can use that link for the next request, to get more commits, and so on.
276
310
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.
278
312
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`.
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:
290
324
291
325
```js
292
326
asyncfunction*fetchCommits(repo) {
293
327
let url =`https://api.github.com/repos/${repo}/commits`;
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 inJSON 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.
319
359
320
360
An example of use (shows commit authors in console):
321
361
@@ -336,7 +376,9 @@ An example of use (shows commit authors in console):
336
376
})();
337
377
```
338
378
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.
0 commit comments