diff --git a/8-async/01-callback-hell/article.md b/8-async/01-callback-hell/article.md new file mode 100644 index 00000000..7cf79efb --- /dev/null +++ b/8-async/01-callback-hell/article.md @@ -0,0 +1,187 @@ + +# Callback hell + +Many things that we do in JavaScript are asynchronous. We initiate a process, but it finishes later. + +The most obvious example is `setTimeout`, but there are others, like making network requests, performing animations and so on. + +Let's see a couple of examples, so that we can discover a problem, and then solve it using "promises". + +[cut] + +## Callbacks + +Remember resource load/error events? They are covered in the chapter . + +Let's say we want to create a function `loadScript` that loads a script and executes our code afterwards. + +It can look like this: + +```js +function loadScript(src, callback) { + let script = document.createElement('script'); + script.src = src; + + script.onload = () => callback(script); + + document.head.append(script); +} +``` + +Now when we want to load a script and then do something, we can call: + +```js +loadScript('/my/script.js', function(script) { + alert(`Cool, the ${script.src} is loaded, let's use it`); +}); +``` + +...And it works, shows `alert` after the script is loaded. + +That is called "a callback API". Our function `loadScript` performs an asynchronous task and we can hook on its completion using the callback function. + +## Callback in callback + +What if we need to load two scripts: one more after the first one? + +We can put another `loadScript` inside the callback, like this: + +```js +loadScript('/my/script.js', function(script) { + alert(`Cool, the ${script.src} is loaded, let's load one more`); + + loadScript('/my/script2.js', function(script) { + + alert(`Cool, the second script is loaded`); + + }); + +}); +``` + +Now after the outer `loadScript` is complete, the callback initiates the inner one. + +...What if we want one more script? + +```js +loadScript('/my/script.js', function(script) { + + loadScript('/my/script2.js', function(script) { + + if (something) { + loadScript('/my/script3.js', function(script) { +*!* + // ...continue after all scripts are loaded +*/!* + }); + } + + }) + +}); +``` + +As you can see, a new asynchronous action means one more nesting level. + +## Handling errors + +In this example we didn't consider errors. What if a script loading failed with an error? Our callback should be able to react on that. + +Here's an improved version of `loadScript` that can handle errors: + +```js run +function loadScript(src, callback) { + let script = document.createElement('script'); + script.src = src; + +*!* + script.onload = () => callback(null, script); + script.onerror = () => callback(new Error(`Script load error ` + src)); +*/!* + + document.head.append(script); +} +``` + +It calls `callback(null, script)` for successful load and `callback(error)` otherwise. + +Usage: +```js +loadScript('/my/script.js', function(error, script) { + if (error) { + // handle error + } else { + // script loaded, go on + } +}); +``` + +The first argument of `callback` is reserved for errors and the second argument for the successful result. That allows to use a single function to pass both success and failure. + +## Pyramid of doom + +From the first look it's a viable code. And indeed it is. For one or maybe two nested calls it looks fine. + +But for multiple asynchronous actions that follow one after another... + +```js +loadScript('/my/script1.js', function(error, script) { + + if (error) { + handleError(error); + } else { + // ... + loadScript('/my/script2.js', function(error, script) { + if (error) { + handleError(error); + } else { + // ... + loadScript('/my/script3.js', function(error, script) { + if (error) { + handleError(error); + } else { + *!* + // ...continue after all scripts are loaded (*) + */!* + } + }); + + } + }) + } +}); +``` + +In the code above: +1. we load `/my/script1.js`, then if there's no error +2. we load `/my/script2.js`, then if there's no error +3. we load `/my/script3.js`, then if there's no error -- do something else `(*)`. + +The nested calls become increasingly more difficult to manage, especially if we add real code instead of `...`, that may include more loops, conditional statements and other usage of loaded scripts. + +That's sometimes called "callback hell" or "pyramid of doom". + +![](callback-hell.png) + +See? It grows right with every asynchronous action. + +Compare that with a "regular" synchronous code. + +Just *if* `loadScript` were a regular synchronous function: + +```js +try { + // assume we get the result in a synchronous manner, without callbacks + let script = loadScript('/my/script.js'); + // ... + let script2 = loadScript('/my/script2.js'); + // ... + let script3 = loadScript('/my/script3.js'); +} catch(err) { + handleError(err); +} +``` + +How much cleaner and simpler it is! + +Promises allow to write asynchronous code in a similar way. They are really great at that. Let's study them in the next chapter. diff --git a/8-async/01-callback-hell/callback-hell.png b/8-async/01-callback-hell/callback-hell.png new file mode 100644 index 00000000..aae83b77 Binary files /dev/null and b/8-async/01-callback-hell/callback-hell.png differ diff --git a/8-async/01-callback-hell/callback-hell@2x.png b/8-async/01-callback-hell/callback-hell@2x.png new file mode 100644 index 00000000..1751e3af Binary files /dev/null and b/8-async/01-callback-hell/callback-hell@2x.png differ diff --git a/8-async/02-promise-basics/article.md b/8-async/02-promise-basics/article.md new file mode 100644 index 00000000..572b1c4b --- /dev/null +++ b/8-async/02-promise-basics/article.md @@ -0,0 +1,206 @@ +# Promise + +There are many ways to explain promises. Here we'll follow the [specification](https://tc39.github.io/ecma262/#sec-promise-objects), because gives real understanding how things work. Besides, you'll become familiar with the terms. + +First, what a promise is, and then usage patterns. + +[cut] + +## Promise objects + +A promise is an object of the built-in `Promise` class. It has the meaning of the "delayed result". + +The constructor syntax is: + +```js +let promise = new Promise(function(resolve, reject) { + // ... +}); +``` + +The function is called *executor*. It is called automatically when the promise is created and can do an asynchronous job, like loading a script, but can be something else. + +At the end, it should call one of: + +- `resolve(result)` -- to indicate that the job finished successfully with the `result`. +- `reject(error)` -- to indicate that an error occured, and `error` should be the `Error` object (technically can be any value). + +For instance: + +```js +let promise = new Promise(function(*!*resolve*/!*, reject) { + // this function is executed automatically when the promise is constructed + setTimeout(() => *!*resolve("done!")*/!*, 1000); +}); +``` + +Or, in case of an error: + +```js +let promise = new Promise(function(resolve, *!*reject*/!*) { + setTimeout(() => *!*reject(new Error("Woops!"))*/!*, 1000); +}); +``` + +Initially, the promise is said to have a "pending" state. Then it has two ways. When `resolve` is called, the state becomes "fulfilled". When `reject` is called, the state becomes "rejected": + +![](promiseInit.png) + +The idea of promises is that an external code may react on the state change. + +The `promise` object provides the method `promise.then(onResolve, onReject)`. + +Both its arguments are functions: + +- `onResolve` is called when the state becomes "fulfilled" and gets the result. +- `onReject` is called when the state becomes "rejected" and gets the error. + +For instance, here `promise.then` outputs the result when it comes: + +```js run +let promise = new Promise(function(resolve, reject) { + setTimeout(() => resolve("done!"), 1000); +}); + +// shows "done!" after 1 second +promise.then(result => alert(result)); +``` + +...And here it shows the error message: + + +```js run +let promise = new Promise(function(resolve, reject) { + setTimeout(() => reject(new Error("Woops!")), 1000); +}); + +// shows "Woops!" after 1 second +promise.then(null, error => alert(error.message)); +``` + +![](promise-resolve-reject.png) + + +- To handle only a result, we can use single argument: `promise.then(onResolve)` +- To handle only an error, we can use a shorthand method `promise.catch(onReject)` -- it's the same as `promise.then(null, onReject)`. + +Here's the example rewritten with `promise.catch`: + +```js run +let promise = new Promise(function(resolve, reject) { + setTimeout(() => reject(new Error("Woops!")), 1000); +}); + +*!* +promise.catch(error => alert(error.message)); +*/!* +``` + +"What's the benefit?" -- one might ask, "How do we use it?" Let's see an example. + +## Example: loadScript + +We have the `loadScript` function for loading a script from the previous chapter, here's the callback-based variant: + +```js +function loadScript(src, callback) { + let script = document.createElement('script'); + script.src = src; + + script.onload = () => callback(null, script); + script.onerror = () => callback(new Error(`Script load error ` + src)); + + document.head.append(script); +} +``` + +Let's rewrite it using promises. + +The call to `loadScript(src)` below returns a promise that settles when the loading is complete: + +```js run +function loadScript(src) { + return new Promise(function(resolve, reject) { + let script = document.createElement('script'); + script.src = src; + + script.onload = () => resolve(script); + script.onerror = () => reject(new Error("Script load error: " + src)); + + document.head.append(script); + }); +} + +*!* +// Usage: +*/!* +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}`); +); +``` + +The benefits compared to the callback syntax: + +- We can add as many `.then` as we want and when we want. Maybe later. +- Also we can pass a promise object somewhere else, and new handlers can be added there, so that's extensible. + +So promises already give us flexibility. But there's more. We can chain promises, see the next chapter. + + +````warn header="Once a promise settles, it can't be changed" +When either `resolve` or `reject` is called -- the promise becomes *settled* (fulfilled or rejected), and the state is final. The result is saved in the promise object. + +Future calls of `resolve/reject` are ignored, there's no way to "re-resolve" or "re-reject" a promise. + +For instance, here only the first `resolve` works: + +```js run +let promise = new Promise(function(resolve, reject) { + resolve("done!"); // immediately fulfill with the result: "done" + + setTimeout(() => resolve("..."), 1000); // ignored + setTimeout(() => reject(new Error("..."), 2000)); // ignored +}); + +// the promise is already fulfilled, so the alert shows up right now +promise.then(result => alert(result), () => alert("Never runs")); +```` + + +```smart header="On settled promises `then` runs immediately" +As we've seen from the example above, a promise may call resolve/reject without delay. That happens sometimes, if it turns out that there's no asynchronous things to be done. + +When the promise is settled (resolved or rejected), subsequent `promise.then` callbacks are executed immediately. +``` + +````smart header="Functions resolve/reject have only one argument" +Functions `resolve/reject` accept only one argument. + +We can call them without any arguments: `resolve()`, that makes the result `undefined`: + +```js run +let promise = new Promise(function(resolve, reject) { + resolve(); +}); + +promise.then(result => alert(result)); // undefined +``` + +...But if we pass more arguments: `resolve(1, 2, 3)`, then all arguments after the first one are ignored. A promise may have only one value as a result/error. + +Use objects and destructuring if you need to pass many values, like this: + +```js run +let promise = new Promise(function(resolve, reject) { + resolve({name: "John", age: 25}); // two values packed in an object +}); + +// destructuring the object into name and age variables +promise.then(function({name, age}) { + alert(`${name} ${age}`); // John 25 +}) +``` + +```` diff --git a/8-async/02-promise-basics/promise-init.png b/8-async/02-promise-basics/promise-init.png new file mode 100644 index 00000000..c54ce05f Binary files /dev/null and b/8-async/02-promise-basics/promise-init.png differ diff --git a/8-async/02-promise-basics/promise-init@2x.png b/8-async/02-promise-basics/promise-init@2x.png new file mode 100644 index 00000000..82f0d713 Binary files /dev/null and b/8-async/02-promise-basics/promise-init@2x.png differ diff --git a/8-async/02-promise-basics/promise-resolve-reject.png b/8-async/02-promise-basics/promise-resolve-reject.png new file mode 100644 index 00000000..5d047ab4 Binary files /dev/null and b/8-async/02-promise-basics/promise-resolve-reject.png differ diff --git a/8-async/02-promise-basics/promise-resolve-reject@2x.png b/8-async/02-promise-basics/promise-resolve-reject@2x.png new file mode 100644 index 00000000..993091a7 Binary files /dev/null and b/8-async/02-promise-basics/promise-resolve-reject@2x.png differ diff --git a/8-async/03-promise-chaining/article.md b/8-async/03-promise-chaining/article.md new file mode 100644 index 00000000..e69de29b diff --git a/8-async/03-promise-chaining/head.html b/8-async/03-promise-chaining/head.html new file mode 100644 index 00000000..e93cd69e --- /dev/null +++ b/8-async/03-promise-chaining/head.html @@ -0,0 +1,10 @@ + diff --git a/8-async/index.md b/8-async/index.md new file mode 100644 index 00000000..7ffeeaf9 --- /dev/null +++ b/8-async/index.md @@ -0,0 +1,5 @@ +development: true + +--- + +# Promises, async/await