diff --git a/6-async/01-callbacks/article.md b/6-async/01-callbacks/article.md index e9960e45..9faa49a2 100644 --- a/6-async/01-callbacks/article.md +++ b/6-async/01-callbacks/article.md @@ -1,10 +1,10 @@ -# Introduction: sync vs async, callbacks +# Introduction: callbacks Many actions in Javascript are *asynchronous*. -For instance, take a look at the function `loadScript(src)` that loads a script: +For instance, take a look at the function `loadScript(src)`: ```js function loadScript(src) { @@ -25,14 +25,14 @@ loadScript('/my/script.js'); The function is called "asynchronous", because the action (script loading) finishes not now, but later. -The call initiates the script loading, then the execution continues normally. +The call initiates the script loading, then the execution continues. While the script is loading, the code below may finish executing, and if the loading takes time, other scripts may run meanwhile too. ```js loadScript('/my/script.js'); // the code below doesn't wait for the script loading to finish ``` -Now let's say we want to use the new script when loads. It probably declares new functions, so we'd like to run them. +Now let's say we want to use the new script when it loads. It probably declares new functions, so we'd like to run them. ...But if we do that immediately after the `loadScript(…)` call, that wouldn't work: @@ -44,12 +44,12 @@ newFunction(); // no such function! */!* ``` -Naturally, the browser probably didn't have the time to load the script. As of now, the `loadScript` function doesn't provide a way to track the load completion. The script loads and eventually runs, that's all. But we'd like to know when happens, to use new functions and variables from that script. +Naturally, the browser probably didn't have time to load the script. As of now, `loadScript` function doesn't provide a way to track the load completion. The script loads and eventually runs, that's all. But we'd like to know when happens, to use new functions and variables from that script. Let's add a `callback` function as a second argument to `loadScript` that should execute when the script loads: ```js -function loadScript(src, callback) { +function loadScript(src, *!*callback*/!*) { let script = document.createElement('script'); script.src = src; @@ -73,7 +73,7 @@ loadScript('/my/script.js', function() { That's the idea: the second argument is a function (usually anonymous) that runs when the action is completed. -Here's a runnable example with the real script: +Here's a runnable example with a real script: ```js run function loadScript(src, callback) { @@ -93,7 +93,7 @@ loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', s That's called a "callback-based" style of asynchronous programming. A function that does something asynchronously should provide a `callback` argument where we put the function to run after it's complete. -Here we did so in `loadScript`, but of course it's a general approach. +Here we did it in `loadScript`, but of course it's a general approach. ## Callback in callback @@ -135,9 +135,11 @@ loadScript('/my/script.js', function(script) { }); ``` +So, every new action is inside a callback. That's fine for few actions, but not good for many, so we'll see other variants soon. + ## Handling errors -In examples above we didn't consider errors. What if a script loading fails with an error? Our callback should be able to react on that. +In examples above we didn't consider errors. What if the script loading fails? Our callback should be able to react on that. Here's an improved version of `loadScript` that tracks loading errors: @@ -172,7 +174,7 @@ Once again, the recipe that we used for `loadScript` is actually quite common. I The convention is: 1. The first argument of `callback` is reserved for an error if it occurs. Then `callback(err)` is called. -2. The second argument and successive ones if needed are for the successful result. Then `callback(null, result1, result2…)` is called. +2. The second argument (and the next ones if needed) are for the successful result. Then `callback(null, result1, result2…)` is called. So the single `callback` function is used both for reporting errors and passing back results. @@ -223,7 +225,7 @@ That's sometimes called "callback hell" or "pyramid of doom". The "pyramid" of nested calls grows to the right with every asynchronous action. Soon it spirales out of control. -So this way of coding appears not so good. +So this way of coding isn't very good. We can try to alleviate the problem by making every action a standalone function, like this: @@ -257,10 +259,12 @@ function step3(error, script) { }; ``` -See? It does the same, and there's no deep nesting now, because we made every action a separate top-level function. It works, but the code looks like a torn apart spreadsheet. It's difficult to read. One needs to eye-jump between pieces while reading it. That's inconvenient, especially the reader is not familiar with the code and doesn't know where to eye-jump. +See? It does the same, and there's no deep nesting now, because we made every action a separate top-level function. -Also the functions named `step*` are all of a single use, they are only created to evade the "pyramid of doom". So there's a bit of a namespace cluttering here. +It works, but the code looks like a torn apart spreadsheet. It's difficult to read, you probably noticed that. One needs to eye-jump between pieces while reading it. That's inconvenient, especially the reader is not familiar with the code and doesn't know where to eye-jump. -We'd like to have a better way of coding for complex asynchronous actions. +Also the functions named `step*` are all of a single use, they are created only to evade the "pyramid of doom". No one is going to reuse them outside of the action chain. So there's a bit of a namespace cluttering here. + +We'd like to have a something better. Luckily, there are other ways to evade such pyramids. One of the best ways is to use "promises", described in the next chapter. diff --git a/6-async/02-promise-basics/article.md b/6-async/02-promise-basics/article.md index 2c558452..6bf1d67b 100644 --- a/6-async/02-promise-basics/article.md +++ b/6-async/02-promise-basics/article.md @@ -8,9 +8,9 @@ Everyone is happy: you, because the people don't crowd you any more, and fans, b That was a real-life analogy for things we often have in programming: -1. A "producing code" that does something and needs time. For instance, it loads a remote script. That's the "singer". -2. A "consuming code" wants the result when it's ready. Many functions may need that result. So that's the "fans". -3. A *promise* is a special JavaScript object that links them together. That's the "list". The producing code makes it and gives to everyone, so that they can subscribe for the result. +1. A "producing code" that does something and needs time. For instance, it loads a remote script. That's a "singer". +2. A "consuming code" wants the result when it's ready. Many functions may need that result. These are "fans". +3. A *promise* is a special JavaScript object that links them together. That's a "list". The producing code creates it and gives to everyone, so that they can subscribe for the result. The analogy isn't very accurate, because JavaScript promises are more complex than a simple list: they have additional features and limitations. But still they are alike. @@ -18,19 +18,17 @@ The constructor syntax for a promise object is: ```js let promise = new Promise(function(resolve, reject) { - // producing code + // executor (the producing code, "singer") }); ``` The function passed to `new Promise` is called *executor*. When the promise is created, it's called automatically. It contains the producing code, that should eventually finish with a result. In terms of the analogy above, the executor is a "singer". -The `promise` object has internal properties: +The resulting `promise` object has internal properties: - `state` -- initially is "pending", then changes to "fulfilled" or "rejected", - `result` -- an arbitrary value, initially `undefined`. -Both `state` and `result` are managed by the executor. - When the executor finishes the job, it should call one of: - `resolve(value)` -- to indicate that the job finished successfully: @@ -42,7 +40,7 @@ When the executor finishes the job, it should call one of: ![](promise-resolve-reject.png) -Here's a simple executor, just to see it in action: +Here's a simple executor, to gather that all together: ```js run let promise = new Promise(function(resolve, reject) { @@ -59,13 +57,15 @@ let promise = new Promise(function(resolve, reject) { We can see two things by running the code above: 1. The executor is called automatically and immediately (by `new Promise`). -2. Executor arguments `resolve` and `reject` are functions that come from JavaScript engine. We don't need to create them (their string representation may vary between JavaScript engines). Instead the executor should call them when ready. +2. The executor receives two arguments: `resolve` and `reject` -- these functions come from JavaScript engine. We don't need to create them. Instead the executor should call them when ready. -After one second of thinking it calls `resolve("done")` to produce the result: +After one second of thinking the executor calls `resolve("done")` to produce the result: ![](promise-resolve-1.png) -The next example rejects the promise with an error: +That was an example of the "successful job completion". + +And now an example where the executor rejects promise with an error: ```js let promise = new Promise(function(resolve, reject) { @@ -76,10 +76,12 @@ let promise = new Promise(function(resolve, reject) { ![](promise-reject-1.png) +To summarize, the executor should do a job (something that takes time usually) and then call `resolve` or `reject` to change the state of the corresponding promise object. + The promise that is either resolved or rejected is called "settled", as opposed to a "pending" promise. ````smart header="There can be only one result or an error" -The executor should call only one `resolve` or `reject`. And the promise state change is final. +The executor should call only one `resolve` or `reject`. The promise state change is final. All further calls of `resolve` and `reject` are ignored: @@ -92,7 +94,7 @@ let promise = new Promise(function(resolve, reject) { }); ``` -The idea is that a job done by the executor may have only one result or an error. In programming, there exist other data structures that allow many "flowing" results, for instance streams and queues. They have their own advantages and disadvantages versus promises. They are not supported by JavaScript core and lack certain language features that promises provide, so we don't cover them here, to concentrate on promises. +The idea is that a job done by the executor may have only one result or an error. In programming, there exist other data structures that allow many "flowing" results, for instance streams and queues. They have their own advantages and disadvantages versus promises. They are not supported by JavaScript core and lack certain language features that promises provide, we don't cover them here to concentrate on promises. Also we if we call `resolve/reject` with more then one argument -- only the first argument is used, the next ones are ignored. ```` @@ -119,7 +121,7 @@ Properties `state` and `result` of a promise object are internal. We can't direc ## Consumers: ".then" and ".catch" -A promise object serves as a link between the producing code (executor) and the consuming code. "Consumers" -- functions that want to receive the result/error can be registered using methods `promise.then` and `promise.catch`. +A promise object serves as a link between the producing code (executor) and the consuming functions -- those that want to receive the result/error. Consuming functions can be registered using methods `promise.then` and `promise.catch`. The syntax of `.then` is: @@ -191,7 +193,7 @@ promise.catch(alert); // shows "Error: Whoops!" after 1 second */!* ``` -So, `.catch(f)` is a complete analog of `.then(null, f)`, just a shorthand. +The call `.catch(f)` is a complete analog of `.then(null, f)`, it's just a shorthand. ````smart header="On settled promises `then` runs immediately" If a promise is pending, `.then/catch` handlers wait for the result. Otherwise, if a promise has already settled, they execute immediately: @@ -203,11 +205,15 @@ let promise = new Promise(resolve => resolve("done!")); promise.then(alert); // done! (shows up right now) ``` -That's good for jobs that may sometimes require time and sometimes finish immediately. The handler is guaranteed to run in both cases. +That's handy for jobs that may sometimes require time and sometimes finish immediately. The handler is guaranteed to run in both cases. +```` -To be precise, `.then/catch` queue up and are taken from the queue asynchronously when the current code finishes, like `setTimeout(..., 0)`. +````smart header="Handlers of `.then/catch` are always asynchronous" +To be even more precise, when `.then/catch` handler should execute, it first gets into an internal queue. The JavaScript engine takes handlers from the queue and executes when the current code finishes, similar to `setTimeout(..., 0)`. -So here the `alert` call is "queued" and runs immediately after the code finishes: +In other words, when `.then(handler)` is going to trigger, it does something like `setTimeout(handler, 0)` instead. + +In the example below the promise is immediately resolved, so `.then(alert)` triggers right now: the `alert` call is queued and runs immediately after the code finishes. ```js run // an immediately resolved promise @@ -218,14 +224,14 @@ promise.then(alert); // done! (right after the current code finishes) alert("code finished"); // this alert shows first ``` -In practice the time for the code to finish execution is usually negligible. But the code after `.then` always executes before the `.then` handler (even in the case of a pre-resolved promise), that could matter. +So the code after `.then` always executes before the handler (even in the case of a pre-resolved promise). Usually that's unimportant, in some scenarios may matter. ```` -Now let's see more practical examples to see how promises can help us in writing asynchronous code. +Now let's see more practical examples how promises can help us in writing asynchronous code. ## Example: loadScript -We have the `loadScript` function for loading a script from the previous chapter. +We've got the `loadScript` function for loading a script from the previous chapter. Here's the callback-based variant, just to remind it: @@ -263,6 +269,7 @@ Usage: ```js run let promise = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js"); + promise.then( script => alert(`${script.src} is loaded!`), error => alert(`Error: ${error.message}`) @@ -276,8 +283,8 @@ We can immediately see few benefits over the callback-based syntax: ```compare minus="Callbacks" plus="Promises" - We must have a ready `callback` function when calling `loadScript`. In other words, we must know what to do with the result *before* `loadScript` is called. - There can be only one callback. -+ Promises allow us to code more naturally. First we run `loadScript`, and `.then` code what we do with the result. -+ We can call `.then` as many times as we want, at any point of time later. ++ Promises allow us to code things in the natural order. First we run `loadScript`, and `.then` write what to do with the result. ++ We can call `.then` on a promise as many times as we want, at any time later. ``` So promises already give us better code flow and flexibility. But there's more. We'll see that in the next chapters. diff --git a/6-async/03-promise-chaining/article.md b/6-async/03-promise-chaining/article.md index 5cb673bf..229a6309 100644 --- a/6-async/03-promise-chaining/article.md +++ b/6-async/03-promise-chaining/article.md @@ -53,7 +53,7 @@ The whole thing works, because a call to `promise.then` returns a promise, so th When a handler returns a value, it becomes the result of that promise, so the next `.then` is called with it. -To make things clear, here's the start of the chain: +To make these words more clear, here's the start of the chain: ```js run new Promise(function(resolve, reject) { @@ -62,13 +62,14 @@ new Promise(function(resolve, reject) { }).then(function(result) { - alert(result); // 1 - return result * 2; // (1) + alert(result); + return result * 2; // <-- (1) }) // <-- (2) +// .then… ``` -The value returned by `.then` is a promise, so we can add another `.then` at `(2)`. When the value is returned in `(1)`, that promise becomes resolved, so the next handler triggers with it. +The value returned by `.then` is a promise, that's why we are able to add another `.then` at `(2)`. When the value is returned in `(1)`, that promise becomes resolved, so the next handler runs with the value. Unlike the chaining, technically we can also add many `.then` to a single promise, like this: @@ -105,7 +106,7 @@ In practice we rarely need multiple handlers for one promise. Chaining is used m Normally, a value returned by a `.then` handler is immediately passed to the next handler. But there's an exception. -If the returned value is a promise, then the further execution is suspended until it settles. And then the result of that promise is given to the next `.then` handler. +If the returned value is a promise, then the further execution is suspended until it settles. After that, the result of that promise is given to the next `.then` handler. For instance: @@ -139,7 +140,7 @@ new Promise(function(resolve, reject) { }); ``` -Here the first `.then` shows `1` returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result is passed on to handler of the second `.then` in the line `(**)`, that shows `2` and so on. +Here the first `.then` shows `1` returns `new Promise(…)` in the line `(*)`. After one second it resolves, and the result (the argument of `resolve`, here it's `result*2`) is passed on to handler of the second `.then` in the line `(**)`. It shows `2` and does the same thing. So the output is again 1 -> 2 > 4, but now with 1 second delay between `alert` calls. @@ -176,7 +177,7 @@ Please note that technically it is also possible to write `.then` directly after loadScript("/article/promise-chaining/one.js").then(function(script1) { loadScript("/article/promise-chaining/two.js").then(function(script2) { loadScript("/article/promise-chaining/three.js").then(function(script3) { - // this function has access to variables script1, script2 and script3 (*) + // this function has access to variables script1, script2 and script3 one(); two(); three(); @@ -187,7 +188,7 @@ loadScript("/article/promise-chaining/one.js").then(function(script1) { This code does the same: loads 3 scripts in sequence. But it "grows to the right". So we have the same problem as with callbacks. Use chaining (return promises from `.then`) to evade it. -Sometimes it's ok to write `.then` directly, because the nested function has access to the outer scope `(*)`, but that's an exception rather than a rule. +Sometimes it's ok to write `.then` directly, because the nested function has access to the outer scope (here the most nested callback has access to all variables `scriptX`), but that's an exception rather than a rule. ````smart header="Thenables" @@ -234,9 +235,9 @@ We'll use the [fetch](mdn:api/WindowOrWorkerGlobalScope/fetch) method to load th let promise = fetch(url); ``` -This makes a network request to the `url` and returns a promise. The promise resolves with a `response` object when the remote server responds with headers, but before the full response is downloaded. +This makes a network request to the `url` and returns a promise. The promise resolves with a `response` object when the remote server responds with headers, but *before the full response is downloaded*. -To read the full response, we should call a method `response.text()`: it returns a promise that resolves when the full text downloaded from the remote server, and has that text as a result. +To read the full response, we should call a method `response.text()`: it returns a promise that resolves when the full text downloaded from the remote server, with that text as a result. The code below makes a request to `user.json` and loads its text from the server: @@ -254,7 +255,7 @@ fetch('/article/promise-chaining/user.json') }); ``` -There is also a method `response.json()` that reads the remote data and parses it as JSON. In our case that's even more convenient. +There is also a method `response.json()` that reads the remote data and parses it as JSON. In our case that's even more convenient, so let's switch to it. We'll also use arrow functions for brevity: @@ -270,7 +271,7 @@ Now let's do something with the loaded user. For instance, we can make one more request to github, load the user profile and show the avatar: ```js run -// 1. Make a request for user.json +// Make a request for user.json fetch('/article/promise-chaining/user.json') // Load it as json .then(response => response.json()) @@ -289,13 +290,13 @@ fetch('/article/promise-chaining/user.json') }); ``` -The code works. But there's a potential problem in it, a typical error of those who begin to use promises. +The code works, see comments about the details, but it should be quite self-descriptive. Although, there's a potential problem in it, a typical error of those who begin to use promises. -Look at the line `(*)`: how can we do something *after* the avatar is removed? For instance, we'd like to show a form for editing that user or something else. As of now, there's no way. +Look at the line `(*)`: how can we do something *after* the avatar has finished showing and gets removed? For instance, we'd like to show a form for editing that user or something else. As of now, there's no way. To make the chain extendable, we need to return a promise that resolves when the avatar finishes showing. -Here's how: +Like this: ```js run fetch('/article/promise-chaining/user.json') @@ -321,7 +322,7 @@ fetch('/article/promise-chaining/user.json') .then(githubUser => alert(`Finished showing ${githubUser.name}`)); ``` -Now when `setTimeout` runs the function, it calls `resolve(githubUser)`, thus passing the control to the next `.then` in the chain and passing forward the user data. +Now right after `setTimeout` runs `img.remove()`, it calls `resolve(githubUser)`, thus passing the control to the next `.then` in the chain and passing forward the user data. As a rule, an asynchronous action should always return a promise. diff --git a/6-async/05-async-await/article.md b/6-async/05-async-await/article.md index 0453a9df..2bdbb148 100644 --- a/6-async/05-async-await/article.md +++ b/6-async/05-async-await/article.md @@ -34,7 +34,7 @@ async function f() { f().then(alert); // 1 ``` -So, `async` ensures that the function returns a promise, wraps non-promises in it. Simple enough, right? But not only that. There's another keyword `await` that works only inside `async` functions, and is pretty cool. +So, `async` ensures that the function returns a promise, wraps non-promises in it. Simple enough, right? But not only that. There's another keyword `await` that works only inside `async` functions, and it's pretty cool. ## Await @@ -67,10 +67,9 @@ f(); The function execution "pauses" at the line `(*)` and resumes when the promise settles, with `result` becoming its result. So the code above shows "done!" in one second. - Let's emphasize that: `await` literally makes JavaScript wait until the promise settles, and then go on with the result. That doesn't cost any CPU resources, because the engine can do other jobs meanwhile: execute other scripts, handle events etc. -It's just a more elegant syntax of getting promise result than `promise.then`, easier to read and write. +It's just a more elegant syntax of getting the promise result than `promise.then`, easier to read and write. ````warn header="Can't use `await` in regular functions" If we try to use `await` in non-async function, that would be a syntax error: @@ -84,13 +83,13 @@ function f() { } ``` -We can get such error when forgot to put `async` before a function. As said, `await` only works inside `async function`. +We can get such error in case if we forget to put `async` before a function. As said, `await` only works inside `async function`. ```` Let's take `showAvatar()` example from the chapter and rewrite it using `async/await`: 1. First we'll need to replace `.then` calls by `await`. -2. And the function should become `async` for them to work. +2. And we should make the function `async` for them to work. ```js run async function showAvatar() { @@ -122,7 +121,7 @@ showAvatar(); Pretty clean and easy to read, right? -Please note that we can't write `await` in top-level code. That wouldn't work: +Please note that we can't write `await` in the top-level code. That wouldn't work: ```js run // syntax error in top-level code @@ -130,12 +129,12 @@ let response = await fetch('/article/promise-chaining/user.json'); let user = await response.json(); ``` -...We need to wrap "awaiting" commands into an async function instead. +So we need to have a wrapping async function for the code that awaits. -````smart header="Await accepts thenables" -Like `promise.then`, `await` allows to use thenable objects (those with a callable `then` method). Again, the idea is that a 3rd-party object may be promise-compatible: if it supports `.then`, that's enough to use with `await`. +````smart header="`await` accepts thenables" +Like `promise.then`, `await` allows to use thenable objects (those with a callable `then` method). Again, the idea is that a 3rd-party object may be not a promise, but promise-compatible: if it supports `.then`, that's enough to use with `await`. -For instance: +For instance, here `await` accepts `new Thenable(1)`: ```js run class Thenable { constructor(num) { @@ -157,9 +156,30 @@ async function f() { f(); ``` -If `await` gets an object with `.then`, it calls that method providing native functions `resolve`, `reject` as arguments. Then `await` waits until one of them is called (in the example above it happens in the line `(*)`) and then proceeds with the result. +If `await` gets a non-promise object with `.then`, it calls that method providing native functions `resolve`, `reject` as arguments. Then `await` waits until one of them is called (in the example above it happens in the line `(*)`) and then proceeds with the result. ```` +````smart header="Async methods" +A class method can also be async, just put `async` before it. + +Like here: + +```js run +class Waiter { +*!* + async wait() { +*/!* + return await Promise.resolve(1); + } +} + +new Waiter() + .wait() + .then(alert); // 1 +``` +The meaning is the same: it ensures that the returned value is a promise and enables `await`. + +```` ## Error handling If a promise resolves normally, then `await promise` returns the result. But in case of a rejection it throws the error, just if there were a `throw` statement at that line. @@ -237,14 +257,14 @@ If we forget to add `.catch` there, then we get an unhandled promise error (and ```smart header="`async/await` and `promise.then/catch`" -When we use `async/await`, we rarely need `.then`, because `await` handles the waiting for us. And we can use a regular `try..catch` instead of `.catch`, that's usually (not always) more convenient. +When we use `async/await`, we rarely need `.then`, because `await` handles the waiting for us. And we can use a regular `try..catch` instead of `.catch`. That's usually (not always) more convenient. -But at the top-level of the code, when we're calling the outmost `async` function, we're syntactically unable to use `await` (as we're not in an `async` function yet), so it's a normal practice to add `.then/catch` to handle the final result or falling-through errors. +But at the top level of the code, when we're outside of any `async` function, we're syntactically unable to use `await`, so it's a normal practice to add `.then/catch` to handle the final result or falling-through errors. Like in the line `(*)` of the example above. ``` -````smart header="Async/await works well with `Promise.all`" +````smart header="`async/await` works well with `Promise.all`" When we need to wait for multiple promises, we can wrap them in `Promise.all` and then `await`: ```js