From ba5424a82dc7f9bf688d43b84a898cc7ac2b4ac0 Mon Sep 17 00:00:00 2001 From: Ilya Kantor Date: Sat, 5 Sep 2020 18:04:40 +0300 Subject: [PATCH] minor fixes --- .../2-async-iterators-generators/article.md | 226 +++++++++++------- 1 file changed, 134 insertions(+), 92 deletions(-) diff --git a/1-js/12-generators-iterators/2-async-iterators-generators/article.md b/1-js/12-generators-iterators/2-async-iterators-generators/article.md index ee4f1eac..59b38c74 100644 --- a/1-js/12-generators-iterators/2-async-iterators-generators/article.md +++ b/1-js/12-generators-iterators/2-async-iterators-generators/article.md @@ -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 , 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:}`. + +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 + } +*/!* +} +``` -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 - } -*/!* -} -``` - -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//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//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