📜 ⬆️ ⬇️

How I made friends with asynchronous JavaScript

JavaScript meets developers asynchronously can be said almost from the threshold. It starts with DOM events, ajax, timers, and library methods associated with animation (for example, jQuery methods fadeIn / fadeOut, slideUp / slideDown). In general, all this is not very difficult and to deal with asynchrony at this stage is not a problem. However, as soon as we turn to writing more or less complex applications that combine all of the above, an asynchronous stream can make it very difficult to understand what is happening in the code. Chains of asynchronous actions, for example, animation> ajax request> initialization -> animation, create a rather complex architecture that does not follow the strict bottom-up direction. In this article I intend to tell you about my experience in overcoming the difficulties associated with asynchronous JS.

I remember, at first, one of the most amazing moments in JavaScript for me was the following:

for(var i=0; i<3; i++){ setTimeout(function(){ console.log(i); }, 0); } 

It was amazing to see:
 3 3 3 

Instead of the expected:
 0 1 2 

Tedious entry

So I realized that asynchrony should not be perceived as something that is executed when you “approximately” expect this (0 milliseconds, for example), and when the execution of the “synchronous” flow (blocking) ends, i.e., “as soon as the opportunity ". It is not easy to make a sensible analogy, since in real life almost all processes are asynchronous. Imagine you are the manager of a construction company. You have received an order to build a house on a turnkey basis, but only a third-party company has permission for a certain type of work on this site (for example, building a house), and you have to contact them. You already have a well-established algorithm: pour the foundation, build a house, paint the house, refine the plot and so on, however, in our case you don’t build a house and don’t even know when it will be built. This is an asynchronous process, your task is to simply transfer the layout to the company and get the finished building. When you build a house, everything is simple, your attention is focused on the current process: construction, then painting and so on. However, now you are not building a house. And you somehow need to organize the work of your team, given the circumstances. This best explains why it’s not worthwhile to block the flow of execution for asynchronous processes — it is idle. If the thread does not block, then as long as an asynchronous action occurs, you can do something else.
')
The most common mistake, newbie, in terms of JavaScript looks like this:

 function Build(layout){ //...         //...   ,   JS  (  house) } function paintRoof(house, color){ house.roof.color = color return house; } var layout = {/*  - */}, house = {}; Build(layout, house); paintRoof(house, 'red'); 

Obviously, it will return a TypeError and say that it cannot read the roof property in undefined, since house.foof will still be undefined (it just simply did not have time to build). We must somehow wait until the house is initialized, but we do not know when this will happen.

I apologize for the tedious introduction, then I will try to explain how in the context of the topic of this article you can solve this problem with the help of JavaScript.

Idea


In fact, there are not so many tools. Returning to the example of a construction company, you, as a manager, know which processes are dependent on each other. All operations with the house (painting, interior arrangement, etc.) are impossible until the house itself is built. However, for example, digging a pool, erecting a fence and some other actions are quite realizable. How to settle the workflow? Obviously, as long as the contractors build the house, we will go about our business, but we also need to decide how we will know when they finish their work (as ridiculous as it sounds in real life). There are two ideas:

From the point of view of JavaScript, there are three options:

Consider these options closer.
I argue that the first option is no good, because it is built on timers and numerous checks. In the simplest case, states are boolean variables. But after all, asynchronous functions can be a processing of an object that has not only 2 states, but much more. And we must respond to each combination of completed states differently. Imagine 3 asynchronous calls that can affect system states only by changing the boolean variable from false to true when the asynchronous action ends. This situation already generates 8 (2 3 ) general system states. You can see for yourself that such an architecture is practically incompatible, and in complex applications, simplicity of layout is often the decisive factor. Checking combinations of states is not an easy task, especially if they are not subject to any logic. Obviously, in terms of cleanliness and clarity of the code, this is a complete nightmare.

You do not want such fragments to flash in your code?
 setTimeout(function(){ if(state1 == 'success' && state2 == 'success'){ ... }else if(state1 == 'success' && state2 == 'error'){ ... }else if(state1 == 'error' && state2 == 'success'){ ... }else if(state1 == 'error' && state2 == 'error'){ ... }else{ setTimeout(arguments.callee, 50); //        } },50 


So what options do we have?

The first option is the Promise way.


It's very simple, we pass the callback function to the asynchronous function that it will call when finished.

 function Build(layout, onComplete){ //... async onComplete(house); } Build(layout, function(buildedHouse){ return house = paintRoof(buildedHouse, 'red'); }); 

This path will lead you sooner or later to PromiseAPI, which provides an opportunity to respond to 2 logical results of completing an asynchronous action: in case of success and in case of error. If you do not implement yourself or do not use ready-made implementations of PromiseAPI (such as Q ), then, by analogy with popular implementations, you can transfer 2 callbacks for asynchronous functions for different results. This way you solve the problem of constant tracking changes. Now, when changes occur, the callback functions work themselves.

 function Build(layout, success, error){ //...   ok - true ,     return ok ? success(house) : error(new Error("-   ")); } Build(layout, function(buildedHouse){ return house = paintRoof(buildedHouse, 'red'); }, function(error){ throw(error); } ); 


This approach also has obvious disadvantages. First, the functions are too intricate in the case of a sequence of asynchronous actions, for example, if there are already 3 of them, then it may look like this:

 async1(args, function(response){ async2(response, function(response){ async3(response, function(response){ hooray(response); //   Node.js. }); }); }); 

And secondly, lack of management flexibility: when developing application pieces, we take responsibility in ensuring the correct execution of callbacks on ourselves. The problem is not even a guarantee, some of the callbacks will work exactly, that's the whole essence of Promise . The problem is that if we again want to assemble competitive events , we do not know which callback will work first, but sometimes it is important to us. Another problem: we register callbacks of some modules inside asynchronous functions of other modules, further, they work and we have to either connect callbacks through the backdoor to the external state of the application, or use global (or adjacent between the modules) data. Both that and other option, we will tell so, not the best idea. We have to carefully design the architecture, specially sharpened to prevent mixing of competitive events, while we could use a much higher level abstraction and apply it in all similar cases. If it immediately occurred to you that you can create some kind of wrapper over an asynchronous stream, then congratulations, the idea of ​​PromiseAPI has reached you. Fortunately, now there is an opportunity to write in the style:

 async1.then(async2).then(async3).then(hooray); 

The most beautiful thing is that you can always choose a design pattern that, as it were, encourages us to use PromiseAPI, including it as our own component. Most modern MV * JavaScript frameworks are based on this. A good example is the $ q service in Angular.js.
As a nishtyak, and if you follow the news, it will not surprise you that some modern browsers already support the native implementation of the promise . Considering the specifications of PromiseAPI is beyond the scope of this article, but I highly recommend reading this article and familiarizing yourself with the Common.js Promises specification.

The second option is Pub / Sub path.


The asynchronous function reports that it ended by posting an event. In this case, we logically separate the part of the code that publishes events from the part of the code that responds to events. This can be justified if the application we write can be clearly logically divided into several modules, each of which performs strictly delineated functionality, but, moreover, they need to interact.
Example
 var manager = {/* */} function Build(layout){ //...  ,    manager.emit({ "type" : "ready", "msg" : " ", "house" : buildedHouse }); } manager.on("ready", function(event){ return house = paintRoof(event.house, 'red'); }); Build(layout); 



At first glance, this approach is not much different from registering callback functions in asynchronous methods, but the main difference is that you yourself control the interaction between the event publisher and the subscriber, which gives some freedom, but is associated with some costs (see the pattern “ Mediator"). The main disadvantage of this approach is that you need an event subscriber who listens on the object for the occurrence of an event and calls a registered callback. It does not have to be a separate object (as in the previous example), there are many implementation options. Often, different “logical layers” arise between modules, - pseudomodules serving the interaction of modules outside the context of other modules. However, in comparison with promises, this approach is more flexible.
An asynchronous function can return an object with a binding method — just like the PromiseAPI, the asynchronous function returns a promise object.
 function Build(layout) { ... return { bind : function(event, callback){ // bind } } } var house = Build(layout); house.bind('ready', function(event){...}); 


Strictly speaking, this is only a matter of implementing the Pub / Sub pattern. How you do it is not so important. If you wrote on NodeJS, you should be familiar with EventEmitter , then you understand how important (and cool!) It is to be able for any class to use the methods of issuing and listening to events. If we are talking about programming for the browser, there are quite a few options. Most likely you, sooner or later, decide to use the trigger methods of the framework you are using, most of the MV * frameworks allow you to do this easily and painlessly (and some :) allow you not to do it at all). In any case, the theory is described in sufficient detail. One of the positive examples is the combination of modular-facade-mediator patterns, more details on this can be found here .

Design


When you start writing more or less large applications, you want to separate the logical parts of the architecture so that you can develop and maintain them separately from each other. In asynchronous programming, the modules do not return the result immediately after calling the API method, and therefore the sequential execution of requests to the module and processing responses by other parts of the application is fundamentally impossible. The ability to register a processor inside the module from the outside is quite a satisfactory method, but you have to understand that if you plan to expand the interaction between modules, you can come to a “callback hell” that should be avoided. On the other hand. Sometimes there are quite simple modules that provide the final API, which is unlikely to scale, then the architecture on the direct implementation of callbacks can also satisfy your requirements.
For example, a module based on jQuery managing ajax-preloader
 var AjaxPreloader = (function(){ function AjaxPreloader(spinner){ this.spinner = spinner; } AjaxPreloader.prototype.show = function(onComplete) { this.spinner.fadeIn(onComplete); return this; }; AjaxPreloader.prototype.hide = function(onComplete) { this.spinner.fadeOut(onComplete); return this; }; return AjaxPreloader; })(); var preloader = new AjaxPreloader($("#preloader")); preloader.show(function(){ div.load("/", preloader.hide); }); 


If you decided to switch to the PromiseApi side, you would get rid of these nestings and with a few modifications you would write like this:
 preloader .show() .then(function(){ return div.load('/') }) .then( function(response){}, //success function(error){} //error ) .always(preloader.hide); 


Very declarative. And we can sleep peacefully, knowing that the AjaxPreloader module will never start returning functions that require an argument, one more callback, and so on. If it is possible to design just such modules, do it. The simpler the modules, and especially their public API, the better.


It is important to be able to understand when to choose some tools, and when others.
Surely you wrote small applications, and you had to use the following scheme:

 var root = $("#container"); // ,     root.on("someEvent", function(){ // }); root.trigger("someEvent"); //    root 

In order not to register callbacks inside any application modules, and especially not to pay attention to the execution context, but, moreover, to preserve the logical separation of application parts, many simply emit a custom event on some DOM element, in order to catch it in some other place the application and perform the necessary actions. Thus, modules depend on only one element, and we, instead of registering callbacks, simply pass an extra parameter to the module — the document element we are going to listen on. Strictly speaking, this is a rather controversial practice, but the idea itself is good. When a module publishes events, and the application listens on them, it is quite convenient in many ways.
For me, it has become commonplace to use a wrapper over objects, which extends modules with standard Pub / Sub methods.

 var module = PubSub({ load: function(url){ ... this.emit('loaded', data); } }); module.on('loaded', function(data){ ... }); 

In this case, the module emits events and is itself a subscriber. The alternative architecture - one subpart for all modules, in general, looks more centralized, but this is just one more layer between the events of the module and the application. Most often this is not necessary, moreover, one should not think that such a repeater is essentially a facade, if it is only collecting events and registering handlers, and this is unconditional (for example, multi-level nesting of the application architecture), then this is only unnecessary interlayer. In addition, another word against a centralized architecture is that composing competitive events in this way becomes harder and harder as the number of application modules increases. I will not simulate rare situations where the application includes modules whose tasks include data synchronization between the server and the client, where there may be several clients, and participants publish competing joint events. I hope you yourself understand how simplified the arrangement of heterogeneous events is when the modules can only be interconnected via a bus of subscriptions and publications. This is very convenient when it comes to components that can interact with each other without the need to pull the centralized control unit.

Event-driven applications


Recently, the actual theme for me is the arrangement of events. The unpleasant side is that events occur not only in different places, but also at different times. To combine disparate events without any special tricks, to put it mildly, is unpleasant. With all this, there is a special kind of application that can be described as “highly event-driven”, which consist of many different parts. These parts can interact with each other, with the user, send data to the server, or just hang in a hold. Attempts to organize all possible combinations of events using traditional imperatives if / then / else is a combinatorial brain explosion. Oddly enough, the methodology of functional programming applied to such applications makes life much easier. There are several libraries that provide the ability to describe complex dependencies between different events in the declarative style of the usual functional programming (see Bacon.JS, Reactive Extensions - RxJS). I will not analyze these libraries in this article, only I will say that now I use a samopisny library, something similar to Bacon.js, but with more emphasis on the layout and destructuring of the asynchronous flow. I provide a fragment of the working code supplied with comments:
A snippet of code from a mini-toy a la Swarmation using a web-socket
 //   var keyups = obj.stream('keyup'), arrowUps = keyups.filter(isArrows); //   function isArrows(which){ ... } //    function vectorDirection(which){ ... } //   event.which    vector2 function allowedMoves(direction){ ... } //    function isWinerPosition(pos){ ... } //    //  enemy.moves = socket.stream('playerMoves'); //    player.moves = arrowUps.filter(allowedMoves).map(vectorDirection); //   game.ticks = timer.stream('tick'); //   game.pause = keyups.filter(function(which){ return which==19}); //     //  game.ticks.syncWith(enemy.moves).listen(redraw); //           player.moves.listen(redraw); //    game.ticks .syncWith(enemy.moves) //       .produceWith(playerMoves, function(pos1, pos2){ //      if(cmp.equals(pos1, pos2)){ //     socket.emit('win'); //    game.ticks.lock(); //    game.emit('loose', { position : pos1, }); } }); game.pause.toggle([ //      function(){ game.ticks.lock(); //    player.moves.lock(); //     socket.emit('pause'); //      - }, function(){ game.ticks.unlock(); //    player.moves.unlock(); socket.emit('run'); } ]); player.moves .filter(isWinnerPosition) //  ,      .listen(function(pos){ game.ticks.lock(); //    socket.emit('loose'); //      game.emit('win', { //   . ! position: pos }); }); 


I will not delve into the essence of this code, this is just an example of how easy (and most importantly, declaratively, you can describe the logic of interactions between objects based on the flow of their events. Therefore, the reader’s question: would you be interested in an article on this topic? I'm going to bring the library and write a demo web application.

Thank you for your attention, may the force be with you!

Source: https://habr.com/ru/post/241161/


All Articles