Skip to main content




La parte central de un ordenador, o be, la parte que lleva a cabo los pasos individuales que componen nuestros programas, se llama processor. The shows we've seen so far are things that will keep the processor until they have finished their work. The speed at which something like a loop manipulating numbers can be executed is highly dependent on the speed of the processor.

But many programs interact with things external to the processor. For example, they can communicate over a computer network or request data from the hard drive, which is much slower than getting it from memory.

When something like this is happening it would be a shame to leave the processor idle as there might be some other work to do in the meantime. In part, this is handled by your operating system, which will switch the processor between multiple running programs. But that doesn't help when we want a single program to be able to progress while waiting for a network request.

Asynchrony

En un modelo de programming sincrónica, las cosas suceden una a la vez. Cuando se llama a una función que realiza una acción de larga duración, sólo vuelve cuando la acción ha finalizado y puede devolver el resultado. Esto detiene el programa durante el tiempo que dure la acción.

An asynchronous model allows multiple things to happen at the same time. When you start an action, the program continues to run. When the action ends, the program is informed and has access to the result (for example, data read from disk).

We can compare synchronous and asynchronous programming using a small example: a program that gets two resources from the network and then combines results.

In a synchronous environment, where the request function only returns after it has done its job, the easiest way to accomplish this task is to do the requests one after the other. This has the disadvantage that the second request will start only when the first has finished. The total time required will be at least the sum of the two response times.

The solution to this problem, in a synchronous system, is to run additional control threads. A thread is another running program whose execution can be interwoven with other programs by the operating system; Since most modern computers contain multiple processors, multiple threads can even run at the same time, on different processors. A second thread could start the second request, and then both threads wait for their results to come back, after which they resynchronize to combine their results.

In the synchronous model, the time taken by the network is part of the timeline for a given thread of control. In the asynchronous model, the start of a network action conceptually causes a split on the timeline. The program that started the action continues to run, and the action occurs alongside it, notifying the program when it ends.

Another way to describe the difference is that waiting for actions to finish is implicit in the synchronous model, while it is explicit, under our control, in the asynchronous one.

Short asynchrony both ways. It makes it easier to express programs that don't fit the straight-line control model, but it can also make expression programs that follow a straight line more cumbersome. We'll look at some ways to address this discomfort later in this chapter.

Las dos importantes plataformas de programación JavaScript (browsers y Node.js) realizan operaciones que pueden tardar un tiempo en ser asincrónicas, en lugar de depender de sub-procesos. Dado que la programación con hilos es notoriamente difícil (entender lo que hace un programa es mucho más difícil cuando está haciendo múltiples cosas a la vez), esto generalmente se considera algo bueno.

Crow Technology

Most people are aware of the fact that crows are very intelligent birds. They can use tools, plan ahead, remember things, and even communicate with each other.

What most people don't know is that they are capable of many things that they keep well hidden from us. A reputed (if somewhat eccentric) expert on corvids has told me that raven technology is not far below human technology, and that they are catching up.

For example, many crow cultures have the ability to build computing devices. These are not electronic, as are human computing devices, but rather operate through the actions of tiny insects, a species closely related to the termite, which has developed a symbiotic relationship with crows. The birds provide them with food, and in return the insects build and operate their complex colonies that, with the help of the living things within, perform calculations.

These colonies are usually located in large and long-lived nests. Birds and insects work together to build a network of bulbous clay structures, hidden among the twigs of the nest, in which the insects live and work.

To communicate with other devices, these machines use light signals. The crows embed pieces of reflective material in special communication stalks, and the insects direct them to reflect the light into another nest, encoding the data as a sequence of rapid flashes. This means that only nests that have an uninterrupted visual connection can communicate.

Our friend the expert in corvids has mapped the network of crow nests in the town of Hières-sur-Amby, on the banks of the Rhone river.

In an amazing example of convergent evolution, raven computers run JavaScript. In this chapter we will write some basic network functions for them.

Callbacks

One approach to asynchronous programming is to make slow-acting functions take an extra argument, a callback function. The action starts, and when it finishes, the callback function is called with the result.

For example, the function setTimeout, available both in Node.js as in the browsers, it waits for a specified number of milliseconds (one second is one thousand milliseconds) and then calls a function.

setTimeout (() => console.log ("Tick"), 500);

Usually, waiting is not a very important type of work, but it can be useful when doing something like updating an animation or checking if something is taking longer than a certain amount of time.

Performing multiple asynchronous actions on a row using the callback function means that you have to keep passing new functions to handle the continuation of the calculation after the actions.

Most computers in crow's nest They have a long-term data storage bulb, where pieces of information are etched onto twigs so that they can be retrieved later. Saving, or searching for data, takes a moment, so the interface for long-term storage is asynchronous and uses callback functions.

Las bombillas de almacenamiento almacenan los datos codificados por JSON bajo nombres. Un cuervo puede almacenar información sobre los lugares donde se esconde la comida bajo el nombre de «cachés de comida», que puede contener una serie de nombres que apuntan a otras piezas de datos, describiendo la cache real. Para buscar un caché de comida en los bulbos de almacenamiento del nido de roble grande, un cuervo podría ejecutar un código como este:

import {bigOak} from "./crow-tech"; bigOak.readStorage ("food caches", caches => {let firstCache = caches [0]; bigOak.readStorage (firstCache, info => {console.log (info);});});

(All names and strings have been translated from the Crow language to English.

This style of programming is doable, but the level of indentation increases with each asynchronous action because it terminates in another function. Doing more complicated things, like running multiple actions at the same time, can be a bit awkward.

Crow's nest computers are built to communicate using request-response pairs. This means that one nest sends a message to another nest, which immediately sends a message back, acknowledging receipt and possibly including an answer to a question asked in the message.

Each message is tagged with a type, which determines how it is handled. Our code can define handlers for specific types of requests, and when a request of this type is received, the handler is called to produce a response.

The interface exported by the module "./Crow-tech" proporciona funciones basadas en la callback para la comunicación. Los nidos tienen un método de envío que envía una petición. Espera el nombre del nido de destino, el tipo de solicitud y el contents de la solicitud como sus tres primeros argumentos, y espera que una función llame cuando se reciba una respuesta como su cuarto y último argumento.

bigOak.send ("Cow Pasture", "note", "Let's caw loudly at 7PM", () => console.log ("Note delivered."));

But to make nests capable of receiving that request, we first have to define a type of request called a "note". The code that handles the requests has to be executed not only in this computer nest but in all the nests that can receive messages of this type. We will assume that a crow flies and installs our code in all the nests.

import {defineRequestType} from "./crow-tech"; defineRequestType ("note", (nest, content, source, done) => {console.log (`$ {nest.name} received note: $ {content}`); done ();});

The function defineRequestType defines a new type of request. The example adds support for "note" requests, which simply send a note to a given nest. Our implementation calls console.log so that we can verify that the request arrived. Nests have a property with a name that contains their name.

The fourth argument given to the handler, done, is a callback function that it should call when the request ends. If we had used the controller's return value as the response value, this would mean that a request controller cannot perform asynchronous actions. A function that performs asynchronous work normally returns before the work is finished, having arranged for a callback to be called when it completes. So we need some asynchronous mechanism (in this case, another callback function) to signal when a response is available.

In a way, asynchrony is contagious. Any function that calls a function that works asynchronously must be asynchronous, using a callback mechanism or the like to deliver its result. Calling a callback is somewhat more complicated and error prone than simply returning a value, so it is no good having to structure large parts of your program that way.

Promises

Trabajar con conceptos abstractos es a menudo más fácil cuando esos conceptos pueden ser representados por valores. En el caso de acciones asincrónicas, en lugar de organizar la llamada de una función en algún momento futuro, se puede devolver un objeto que represente este event futuro.

This is what the standard Promise class is for. A promise is an asynchronous action that can complete at some point and produce a value. It is able to notify anyone who is interested when its value is available.

The easiest way to create a promise is by calling Promise.resolve. This function ensures that the value given to it is wrapped in a promise. If it is already a promise, it is simply returned; otherwise you get a new promise that immediately ends its value as a result.

let fifteen = Promise.resolve (15); fifteen.then (value => console.log (`Got $ {value}`)); // → Got 15

To get the result of a promise, you can use its then method. This registers a callback function to be called when the promise resolves and produces a value. You can add multiple callbacks to a single promise, and they will be called, even if you add them after the promise has already been resolved (finished).

But that's not all the method does then. Returns another promise, which resolves the value that the controller function returns or, if that returns a promise, waits for that promise and then resolves its result.

It is useful to think of promises as a device to translate values into asynchronous reality. A normal value is just there. A promised security is a security that may already exist or may appear at some point in the future. Calculations defined in terms of promises act on these wrapped values and run asynchronously as the values become available.

To create a promise, you can use Promise as a constructor. It has a somewhat strange interface: the constructor expects a function as an argument, which it calls immediately, passing it a function that it can use to resolve the promise. It works this way, rather than, for example, with a resolve method, so that only the code that created the promise can resolve it.

This is how you create a promise-based interface for the function readStorage:

function storage (nest, name) {return new Promise (resolve => {nest.readStorage (name, result => resolve (result));}); } storage (bigOak, "enemies") .then (value => console.log ("Got", value));

This asynchronous function returns a significant value. This is the main advantage of promises: they simplify the use of asynchronous functions. Instead of having to pass callbacks, promise-based functions look similar to normal ones: they take the data as arguments and return their results. The only difference is that the output may not be available yet.

Failure

Regular JavaScript calculations can fail if an exception is made. Asynchronous calculations often need something like this. A network request may fail, or some code that is part of the asynchronous computation may be an exception.

One of the most pressing problems with the callback style of asynchronous programming is that it makes it extremely difficult to ensure that faults are properly reported to callbacks.

A widely used convention is that the first argument of the callback is used to indicate that the action failed, and the second contains the value produced by the action when it was successful. These callback functions should always check for an exception and make sure that any problems they cause, including exceptions thrown by the functions they call, are caught and assigned to the correct function.

Promises make it easier. They can be resolved (the action completed successfully) or rejected (failed). Resolve handlers (as registered at the time) are called only when the action is successful, and rejections are automatically propagated to the new promise that is returned by then. And when a handler throws an exception, it automatically causes the promise produced by its call to be rejected. So if any element in a chain of asynchronous actions fails, the result of the entire chain is marked as rejected, and no success handler is called beyond the point at which it failed.

In the same way that resolving a promise provides a value, rejecting one also provides one, usually called the reason for the rejection. When an exception in a handler function causes rejection, the exception value is used as the reason. Similarly, when a handler returns a promise that is rejected, that rejection flows to the next promise. There is a function Promise.reject which creates a new promise, immediately rejected.

To explicitly handle such rejections, promises have a catch method that registers a handler to be called when the promise is rejected, similar to how handlers handle normal resolution. It is also very much like that in the sense that it returns a new promise, which resolves to the value of the original promise if it resolves normally, and to the result of the catch handler otherwise. If a catch manager throws an error, the new promise is also rejected.

For short, it also accepts a reject handler as the second argument, so you can install both types of handlers in a single method call.

A function passed to the Promise constructor receives a second argument, along with the resolve function, that it can use to reject the new promise.

The strings of promise values created by the calls at that point and the catch can be seen as a pipeline through which asynchronous values or failures move. Since these chains are created by registering handlers, each link has a success handler or a reject handler (or both) associated with it. Handlers that do not match the type of result (success or failure) are ignored. But the ones that do match are called, and their result determines what kind of value comes next: success when it returns a value with no promise, rejection when it throws an exception, and the result of a promise when it returns one of those.

new Promise ((_, reject) => reject (new Error ("Fail"))) .then (value => console.log ("Handler 1")) .catch (reason => {console.log ("Caught failure "+ reason); return" nothing ";}) .then (value => console.log (" Handler 2 ", value)); // → Caught failure Error: Fail // → Handler 2 nothing

Just as an uncaught exception is handled by the environment, JavaScript environments can detect when a promise rejection is not handled and will report it as an error.