|
1 | 1 |
|
2 | 2 | # Async iteration and generators |
3 | 3 |
|
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. |
5 | 5 |
|
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. |
7 | 7 |
|
| 8 | +Let's see a simple example first, to grasp the syntax, and then build a real-life example. |
8 | 9 |
|
9 | | -For |
10 | | -Either we need to download or upload something, or we need |
| 10 | +## Async iterators |
11 | 11 |
|
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. |
13 | 13 |
|
14 | | -But they don't solve |
| 14 | +Here's a small cheatsheet: |
15 | 15 |
|
| 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` | |
16 | 21 |
|
| 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: |
17 | 23 |
|
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, |
19 | 28 |
|
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. |
0 commit comments