This commit is contained in:
Ilya Kantor 2017-05-30 02:16:13 +03:00
parent 455d300d8d
commit 58c04e2d2d
4 changed files with 99 additions and 67 deletions

View file

@ -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.