📜 ⬆️ ⬇️

About using $ .Deferred to work with asynchronous tasks

Hello to all!

In this article, I would like to share with you thoughts on how, in practice, you can use the asynchronous process mechanism provided by the jQuery library from version 1.5 called deferred, “deferred”, as well as with related objects and methods .

Of course, more than a dozen articles have already been written on the topic of working with a deferred / promise pair. I set myself my goal to provide such a set of knowledge that would give a beginner, firstly, the opportunity to forget about their fears of incomprehensible and complex and, secondly, to take another step towards writing clear and well-structured code that works with asynchronous processes . I would like to focus my and your attention on the problems that are easily resolved using deferred, on the assumptions and typical patterns of using this object.

Asynchronous processes


So, an asynchronous process assumes that it can be executed in parallel with other code, and the result of its execution is not immediately available to the calling program. This, of course, is strange : are not synchronous actions, those that are performed in parallel with each other, and not sequentially, one after the other? To justify established terminology: saying that the process is “synchronous,” we proceed from the fact that the moment when its result appears, i.e. The end of one process, coincides with the moment of the beginning of the execution of some next, and their end and the beginning are synchronized .
')
var result = readValueBy(key); console.info(result); 

And the “asynchronous” property says that the result of the process will come, but the exact place in the program where this happens cannot be specified. To describe the logic of processing the results of an asynchronous process, programmers resort to callback functions, or callback functions, transferring the results of work to them in the form of its actual input parameters.

An example of an asynchronous process


We are used to the fact that an asynchronous process can be an ajax request. And when we are faced with the task of writing a call to the server using the convenience of jQuery, we will write about the following, without a second thought:

 $.post("ajax/test.html", function( data ) { $( ".result" ).html( data ); }); 

Moreover, this is an example from the official jQuery documentation for the $.post() method ( see ).
Note that the second parameter in the method is a callback function, which takes the result as the first argument - the server response.

Here is an alternative version of such a request:

 $.ajax({ type: "POST", url: "ajax/test.html", data: data, success: function( data ) { $( ".result" ).html( data ); }, dataType: dataType }); 

In this example, the successful completion handler is passed as a property of the object along with other parameters.

But, starting with jQuery 1.5, ajax methods ($ .get, $ .post, $ .ajax) can be called without passing them handlers, since these methods return not just an XMLHTTPRequest object, but this object, overloaded with methods that implement the Promise interface . From a practical point of view, this means that by initiating an asynchronous process (= request to the server), we can not hurry to indicate callbacks to successful execution, to handle an error situation, etc., and not to limit ourselves to one when we need to specify them . We can save the server request in a variable:

 function api(key, data) { return $.post('ajax/' + key, data); } // .. var requestData = { id: '79001' }, storeDataRequest = api('get_store_by_id', requestData); //    

... and subsequently "hang" all the callbacks we need:

 function addStandardHandlers(request, requestData, model) { request.done(function(data){ //   ,        //        model.received(requestData, data && data.payload); }).fail(function(){ //      //   ,        model.received(requestData, null); }); } function addUIHandlers(request, $messageHolder) { //   .. request.done(function(data){ // ..   "", $messageHolder.text('Success'); }).fail(function(){ //    - // ",  " $messageHolder.text('Error, please retry.'); }).always(function(){ //   ,   ,   , //        window.setTimeout(function(){ $messageHolder.fadeOut(); }, 3000); }); } //  ,    addStandardHandlers(storeDataRequest, requestData, context.getModel()); //  ,   UI addUIHandlers(storeDataRequest, $('.main_page-message_holder')); 

If there is a hundred and one place in our program where we need to contact the server, and the response structure everywhere requires the same type of processing, it is logical not to produce the same code as the handlers in the server call sites, but to “hang” for each instance of the request depending on the need, using knowing that ajax requests are now promises. See the promises below.

Other asynchronous processes


Let's step back and list some other asynchronous processes that we have to deal with:



What problems does Deferred solve?


Let us think about situations when we need something more than passing a callback as a parameter to a function or an object property (as we saw in simple examples of using ajax above):

  1. How to assign more than one callback for the same process completion event?
  2. How to recognize the onset of the expected event after its completion?
  3. How to group all callback handlers according to the types of the processed situation around a single process?


I remember how long ago we solved the problem of the first item for multiple onload event handlers:

 window.onload = (function(oldHandler){ return function(){ if (oldHandler) oldHandler(); // new code here.. }; })(window.onload); 

Well, that is already a problem of bygone days. Today, no one does (hopefully) and it’s enough to remember about $ .on (..) (working with jQuery) if we are talking about document events. And about $ .Deferred () - when we talk about everything else. (Sorry for deliberately exaggerating, it serves the purpose of the story.) Well, “what to do with multiple handlers?” Is another question. Suppose one, for example, you can parse the data, and the other - hide the busy indicator or change the status text, as suggested in the example above. Previously, we had to shove everything, and work with the model, and with the display, in one handler, now we have the freedom to share.

Also, several years ago, the second item was solved by some sort of clumping around the flag variable, to which the body of the handler was closed. I clearly remember how we thought, how to tell our widget that dom ready had already taken place by the time it was uploaded. But there was $(function(){ ... }) , which is now implemented in jQuery using deferred. And this means that the callback in this construction will work even when the code is executed after the occurrence of the dom ready event itself. The scheme of use is simple. "Hang" handler. If the process is already terminated, the handler is executed immediately. If not, the handler (all handlers of this type) will be executed at the time of its occurrence in the future.

Let's look towards deferred to solve the third problem . With deferred, we have a group of handlers to successfully complete the process, there is - for an unsuccessful (erroneous or unforeseen) completion, there is a group that will be called at any ending (“love”). There is also a group in which handlers can track the progress of the operation.

So, we looked at three problems solved using $ .Deferred ().

Prerequisites for using Deferred


Let's mark the conditions when we need to think about using deferred in our program.

We will think about using Deferred then ..

  1. when we need to wait for the onset of a state and save the indicator of its occurrence and / or related data. Waiting for a condition to occur is the first condition.
  2. The second condition is when we think about the relative position of the code of a lengthy process and the code of processing its change / completion. Deferred allows you to "untie" at the place of determining the method of obtaining data / results from determining the method of their reception and subsequent processing.


Notice that the distinguishing feature of the first condition is the onset of the state , and not just the moment at which it happened. This distinguishes Deferred from jQuery's regular event handling mechanisms. The event occurred, caused handlers, and is forgotten. The state of the process defined by Deferred, having arrived, is preserved as long as we store Deferred.

The second premise. Let's take a look at the code:

 $.post("ajax/cart", function( data1 ) { //  1. //   data1 // if (data1 && data.success) .... //  2. //      var processedData1 = data1.payload; //  3. //      : $.post("ajax/shipping", processedData1, function( data2 ) { //  1. //  2. var processedData2 = data2.payload; //  3. $.post("ajax/payment", data2, function( data3 ) { //  1. //  2. var processedData3 = data3.payload; //  3.    $( ".result" ).html( processedData3 ); }); }); }); 

The first request receives data on the basis of which the second request is made, and on the basis of its result, subsequent ones, etc. There are only three requests and the logic is only indicated, but we already have the famous anti-pattern “Pyramid of Evil Rock” (pyramid of doom).

In one of the projects in which I had the opportunity to participate, the task of “wrapping up” a hidden payment system site (which was written in CGI and was unsupported, was solved, so there was no talk of styling, modifying and embedding it in our site) on WordPress, which is “in the shadow,” using asynchronous get- and post-requests, emulated user actions (entering the cart page, recalculating goods, sending a form, receiving a confirmation page, sending an address, sending a second form, entering payment data ..) I know I know. This is bad. I am ashamed to recall the details. At one time I was afraid to go there and refactor, because "It worked." But the most terrible thing is that the method containing the payment logic had either six, or seven levels of nesting and stretched screens by ten in height. What can we say, with this solution, the choice between supporting only this method and mastering CGI from scratch became not obvious, with a slight advantage in favor of CGI. What would I do with a similar decision now (let's pretend that imitating the sequence of making a purchase is “okay”)?

First, I would break the logic of the method into steps. Let me demonstrate, based on the example above:

 function purchaseStep01_CartDataRequestFor(input) { return $.post("ajax/cart", input); } function purchaseStep02_UpdateShippingInfoRequestFor(input) { return $.post("ajax/shipping", input); } function purchaseStep03_SubmitPaymentDetailsRequestFor(input) { return $.post("ajax/payment", input); } 

Secondly, the request is not enough to send, it is necessary to provide as a general functionality for the analysis of answers:

 //   : // -  -  -  //   /   function handleResponseData(request, specificLogic) { //    -promise,    fail //   ,  AJAX  ,   //        success==true // (          ) //      ,      return $.Deferred(function(def){ request .fail(def.reject) .done(function(data){ if (!data || !data.success) return def.reject(); if (!specificLogic) return def.resolve(data); try { def.resolve(specificLogic(data)); } catch (e) { def.reject(e); } }); }).promise(); } 

... and the logic for processing the result of each request separately. This logic is compatible with the above:

 //  1.   .  . var step01_CartDataPromise = handleResponseData( purchaseStep01_CartDataRequestFor(data), function (data){ //       . //    -  ,    fail. if (!data.cart) throw new Error('Cart info is missing'); //  -  ,     cartInfo  . return data.cart; }), // <- , ..         . //  2.       1. //       : // step02_ShipInfoPromise = step01_CartDataPromise.then().done(function(cartInfo){... // then()   ,      // .      jQuery step02_ShipInfoPromise = step01_CartDataPromise.then(function(cartInfo){ return handleResponseData( purchaseStep02_UpdateShippingInfoRequestFor({ id: cartInfo.id}), function (data){ if (!data.shipping) throw new Error('Shipping info is missing'); return { cart: cartInfo, shipping: data.shipping }; }); }), //  3.     2. step03_PaymentResultPromise = step02_ShipInfoPromise.then(function(prePurchaseInfo){ return handleResponseData( purchaseStep03_SubmitPaymentDetailsRequestFor(prePurchaseInfo), function (data){ if (!data.payment) throw new Error('Payment gone wrong'); return data.payment; }); }); // <-    //          ,  //       //     done()  then()  function(paymentInfo)..  //    function(prePurchaseInfo)..   . // .    2. step03_PaymentResultPromise.then(function(paymentInfo){ $('.result').html( paymentInfo.message ); }); 


Cons of such a decision:



Pros:



Few would argue that the advantages outweigh both the number and quality.

It seems I have demonstrated the background. Let's go further. Before proceeding with the examples, I promise a few words about promises.

Promise


Having a reference to the deferred object, the program has the ability to control the result of the process flow (not the process itself, although sometimes it) and “inform” all listeners how it ended. But the promise object, or “promise”, associated with this deferred object, gives the program a set of methods exclusively for “hearing”. (It is interesting to note that in a Wikipedia article on this topic such a readonly object is called futures (future), and the promise is what $. Deferred in jQuery. Try not to get confused.) In fact, this separation of powers warns a number of awkward situations when the code wants to do what he is “not supposed to do.”

 var def = $.Deferred(); //   , //  "" def.resolve()  def.reject() // //  readonly- promise, //   def //  ""  var promise = def.promise() 

Most often, the most convenient and secure way to use the reference to deferred is to define its logic within the function passed by the parameter when creating it:

 $.Deferred(function(def){ //       , //  resolve()  reject() //    . }); 

Such use can immediately be “given away” (return), not forgetting to close access to this object by calling .promise() .

A simple example. The onset of the timeout determines the successful completion of the process.

 function doneTimeout(timeout) { return $.Deferred(function(def){ window.setTimeout(def.resolve, timeout); }).promise(); } 

It seems that I have fulfilled my promise to tell about the promises. Examples

Examples


I will say that some time ago I absolutely could not think of deferred / promise concepts and drew inspiration from the examples on the official website .

Loading pictures


In particular, there I became acquainted with the procedure for controlling the loading of images. But then it occurred to me to add it so that it would be possible to recognize not only whether the picture is loaded or not, but also its dimensions:

 var loadImage = createCache(function(defer, url) { var image = new Image(); function cleanUp() { image.onload = image.onerror = null; } defer.then( cleanUp, cleanUp ); image.onload = function() { defer.resolve( url, { width: image.width, height: image.height }); }; image.onerror = defer.reject; image.src = url; }); 


Determining the occurrence of a document readiness


We already talked about $(function(){.. }) above. Let me just give a way to get the object-promise of the availability of the document in its pure form:

 var domReadyPromise = $.ready.promise(); 


Onset of timeout state


We have already considered an example with a timeout, the occurrence of which entails the successful completion of the process, doneTimeout() . But now we also need an option, in which the timeout will be considered that the process is completed with an error. Here he is:

 function failTimeout(timeout) { return $.Deferred(function(def){ window.setTimeout(def.reject, timeout); }).promise(); } 

We can implement the logic of the button, which does not care whether you press it or not, because after 10 seconds it will click. Does it happen? Who can this come to mind?

 var timeToSubmit = $.Deferred(function(def){ doneTimeout(10000).done(def.resolve); $('.submissionButton').one('click', function(){ def.resolve(); return false; }); }).promise(); timeToSubmit.done(function(){ //       //    }); 


Completion of the animation on the page elements


Sometimes I need to consider the process completed not at the moment when the data from the server arrives, but when we accurately show it with a pop-up message and then this message is extinguished. We can, of course, make a binding like this (this is a revised example with one of the links at the end of the note):

 function animationPromise($element){ return $.Deferred(function(def){ $element.fadeIn( 10000 , def.resolve ); }).promise(); } 

however, it is completely unnecessary. Each jQuery element supports a promise () call, returning a promise to end the animation on it:

 function animationPromise($element){ return $element.fadeIn( 10000 ).promise(); } 

"Listen" to the end of the animation can be as follows:

 animationPromise($('#foo')).done(function(){ console.log('Animation is finished'); }); 

or simply:

 $element.fadeIn( 10000 ).promise().done(function(){ console.log('Animation is finished'); }); 

Do not forget that in this way you should not “string” one animation onto another. To do this, there are more concise regular ways:

 $element.fadeIn( 10000 ).fadeOut( 10000 ); 


Maintenance of the modal dialogue


There are many examples of libraries that improve user interaction with the interface of the page, which, however, sometimes forget about programmers.

An example is the implementation of a modal window operation in Twitter Bootstrap ( see here ). There are methods - “show” and “hide”, there are events “window open”, “window hidden”. But how can we, programmers, find out what choice the user made in this window? Those. , ? ? , Cancel, , , , , .

Twitter Bootstrap, , , . .


. : - . , , .

jQuery $.when() , , , , — . — . (=) , , — (=).

, , . Those. , , , . .

:

 // fail if and only if all fail: function failIifAllFail(_promise_array){ var promises = [].slice.apply(arguments), count = promises.length; return $.Deferred(function(def){ //      var done = 0, fail = 0; //             //   check() ,       //     $.each(promises, function wrap(key, p){ p.done(function(){ done++; check(); }).fail(function(){ fail++; check(); }); }); function check(){ if ((done + fail) < count) return; if (fail === count) def.resolve(); else def.reject(); } }).promise(); } 



— , , , — . , , , , , -, - , — . . — .

, , .

Thanks for attention!

Related links


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


All Articles