
It so happened that the last few weeks very often I had to hear the words
Promise and
Deferred from different people. As a rule, these concepts are used by developers that have already been seen, who are faced with certain tasks in their activities.
As I can tell, for people who in practice have
not encountered some specific problems, these 2 concepts are quite difficult to understand. And not because the concepts of Promise and Deferred are complicated with something, but because it is rather difficult to invent a suitable task right away in order to try Deferred objects (in jQuery and not only) in action.
Yes, probably for those who are familiar with this question, it will seem trivial and not worth it and the eaten away egg. In addition, the issue has already been discussed many times. However, I will have the courage to touch it again and here is why:
1) It seems to me that this post may be interesting for some readers.
2) I will go from practice, not from theory. My task is to demonstrate the operation of the tool. The theory and other applications, if necessary, you will find in the links to the post.
')
Below I will try to show you that Promise and Deferred are very, very simple. In addition, to explain this topic, I will have to touch on a few more interesting points in JavaScript.
What is a Deferred object? : this is a very simple way to catch the states of asynchronous events.
Why use them? : for example, to assign a common callback to several AJAX requests.
Where can this be used? : for example, you want to show a map on the page. But only when received information about all the marks on it from an external source. Suppose you load a page and a map (hidden) and show a preloader, at this point the browser sends 20 AJAX requests to your server and receives information. And only after the completion of all requests you need to hide the preloader, show the map, markers and blocks of content. This is how an abstract task can be formulated.
Next, I will show examples in CoffeeScript (with JavaScript under the spoiler) and I should clarify some points regarding CoffeeScript.
1) CoffeeScript is absolutely the same JavaScript, but with a cleaner and more concise syntax.
2)
-> in CoffeeScript means
function () {} in JavaScript
3)
(a, b) -> in CoffeeScript means
function (a, b) {} in JavaScript
4)
do in CoffeeScript means
() in JavaScript. Ie the function call, which name is specified after
do5)
do (a, b) -> "Hello World!" in CoffeeScript means
(function (a, b) {return "Hello World!";}) (a, b) in JavaScript
Apply method
I am lazy and do not like to once again knock fingers on the keys. Therefore,
console.log I prefer to use just
log . This is easily achieved using the JS method
apply .
What is the apply method? This is one of the Function methods (Function.apply). So, it applies to all functions.
What does the apply method do? It is called from a function and therefore affects it.
The first argument sets the context of the function being called, i.e. actually forcibly sets the value of
this inside this function.
The second argument is an array of parameters for the function.
Where can this be used? Often in the description of functions you can see that it accepts a list of parameters
fn (x1, x2, x3, x4, ...) as input, but by a strange coincidence you may have an array of variables and you would like to pass this array to a function in as a list, not as a single array variable. This is exactly what the
apply method
does . In essence, apply allows you to expand an array of variables into a function parameter list.
Knowing that the array of all values ​​passed to the function in JavaScript can be obtained through the
arguments variable, let's make the following feint with our ears:
window.log = -> try console.log.apply(console, arguments)
Js version window.log = function() { try { return console.log.apply(console, arguments); } catch (_error) {} };
What happened here? I created a
log method in the global scope. All the arguments that I get (arguments) when I call the
log method I pass as a list to the
console.log function (the apply method helps me). In addition, in order not to violate the logic of the
console.log method, I preferred to pass the
console object as the first argument to the
apply function. For browsers that have problems with
console.log, I use
try to protect myself.
Some more sugar in CoffeePut
... after the arguments array and you will save yourself from specifying the context and manual writing apply. This code is completely identical to the one presented above.
window.log = -> try console.log arguments...
If this is all more or less clear, then move on. By the way, I didn’t just remember how to
apply - it is still useful to us.
Step 1. Classical problem
To emulate 10 asynchronous requests, use the
setTimeout method . When performing, he breaks away from the main stream of performance and "lives his life." So we found a great option to test the operation of Deferred objects.
Let's make a simple loop and run setTimeout 10 times and prologue the
index variable.
for index in [0...10] setTimeout -> log index , 1000
Js version var index, _i; for (index = _i = 0; _i < 10; index = ++_i) { setTimeout(function() { return log(index); }, 1000); }
Exactly in a second, we will receive not the numbers
"0 1 2 3 4 5 6 7 8 9" , but
"10 10 10 10 10 10 10 10 10 10" .
This happened because by the time the code inside
setTimeout is executed, the cycle is already over (it will work very quickly), and the
index counter at this point will take the extreme value and will be equal to 10.
Step 2. The classic solution
In order to solve the above problem, you simply wrap the executable code in a function and call it, passing the current value of the counter. Voila! In a coffeescript, these actions are quite elegant.
for index in [0...10] do (index) -> setTimeout -> log index , 1000
Js version var index, _fn, _i; _fn = function(index) { return setTimeout(function() { return log(index); }, 1000); }; for (index = _i = 0; _i < 10; index = ++_i) { _fn(index); }
Step 3. How does deferred work?
A deferred object is just a repository for the state of an asynchronous function. There are usually several such conditions:
-
pending - waiting for completion of the process
-
rejected - the process is completed by dropping
-
resolved - process completed successfully
In addition, the Deferred object has a number of methods that can change its state. For example, the
.resolve () method.
By the state of the Deferred object, we can judge whether the process whose state we are monitoring is complete.
From this we can conclude that in order to use the Deferred object, we must:
1) create a Deferred object
2) When completing the asynchronous method, transfer the Deferred object to the desired state
3) Send the Deferred object from the current function to somewhere where its status will be monitored.
Below you see the code that illustrates this. I leave a detailed analysis to the interested reader. About
return dfd.promise () read immediately after the sample code.
for index in [0...10] promise = do (index) -> dfd = new $.Deferred() setTimeout -> log index dfd.resolve() , 1000 return dfd.promise()
Js version var index, promise, _i; for (index = _i = 0; _i < 10; index = ++_i) { promise = (function(index) { var dfd; dfd = new $.Deferred(); setTimeout(function() { log(index); return dfd.resolve(); }, 1000); return dfd.promise(); })(index); }
You should definitely pay attention to the
return dfd.promise () string. And for sure you will ask a reasonable question: “why, when returning from a function, it is not the Deferred object itself that is transferred, namely the
Promise ?”. The bottom line is that when transferring a Deferred object to the outside, you must exclude from it all methods that can change its state. Since nowhere, except for the strictly specified allowed area, a Deferred object should not be able to change its state. Otherwise, the final developer may not resist the temptation and break the given pattern.
Promise is exactly what is involved in returning a reduced copy of Deferred.
Step 4. We form an array of "promises"
If we need to wait for 10 asynchronous functions to be executed, we will simply form an array of “promises” (apparently promises to finish our execution once), which are formed in the functions inside the loop. And so, we create an empty array and enter into it a new “promise” at each iteration.
promises_ary = [] for index in [0...10] promise = do (index) -> dfd = new $.Deferred() setTimeout -> log index dfd.resolve() , 1000 dfd.promise() promises_ary.push promise log promises_ary
Js version var index, promise, promises_ary, _i; promises_ary = []; for (index = _i = 0; _i < 10; index = ++_i) { promise = (function(index) { var dfd; dfd = new $.Deferred(); setTimeout(function() { log(index); return dfd.resolve(); }, 1000); return dfd.promise(); })(index); promises_ary.push(promise); } log(promises_ary);
As a result,
log will show us a list of objects.
# => [obj, obj, ...]
Step 5. Use the array of "promises"
And now it is very simple to hang up a callback, which will be called only after the execution of all asynchronous functions. To do this, we need the jQuery method
$ .when . The
$ .when documentation is described as a method that accepts a parameter list for input. However, we have an input array of
promises_ary parameters. As it is not difficult to guess, we again use the
apply method.
promises_ary = [] for index in [0...10] promise = do (index) -> dfd = new $.Deferred() setTimeout -> log index dfd.resolve() , 1000 dfd.promise() promises_ary.push promise $.when.apply($, promises_ary).done -> log 'Promises Ary is Done'
Step 6. A bit of randomness
In the end, of course, you should add a bit of “randomness” to our setTimeout function. Let's make a simple
helper method that will return random numbers and will execute the code inside setTimeout with a random delay.
rand = (min, max) -> Math.floor(Math.random() * (max - min + 1) + min) promises_ary = [] for index in [0...10] promise = do (index) -> dfd = new $.Deferred() setTimeout -> log index dfd.resolve() , rand(1, 5) * 1000 dfd.promise() promises_ary.push promise $.when.apply($, promises_ary).done -> log 'Promises Ary is Done'
Js version var index, promise, promises_ary, _i; rand = function(min, max) { return Math.floor(Math.random() * (max - min + 1) + min); }; promises_ary = []; for (index = _i = 0; _i < 10; index = ++_i) { promise = (function(index) { var dfd; dfd = new $.Deferred(); setTimeout(function() { log(index); return dfd.resolve(); }, rand(1, 5) * 1000); return dfd.promise(); })(index); promises_ary.push(promise); } $.when.apply($, promises_ary).done(function() { return log('Promises Ary is Done'); });
In conclusion, it should be said that all AJAX requests in jQuery from version 1.5 use the Deferred / Promise mechanism, so working with them is even easier.
I will stop at this. The basic example with explanations, in my opinion, is completed. I think this will be enough to continue a deeper study of Deferred / Promise equipment from other sources. I hope this post once will help someone.
- Deferred Object (JQuery official)
- jQuery Deferred Object (Habr)
- Using Deferred Objects in jQuery 1.5 (Habr)
- CommonJS Promises
- AngularJS (promise / deferred)
- Deferred objects in AngularJS
- Promises in AngularJS
- Promises for ActionScript 3.0
- Object deferred
Question for discussion: Often it is necessary to meet the statement that Deferred can be used not only together with asynchronous functions, but also with synchronous ones. At the moment I have not met the tasks where it can be useful. If you have a similar practice, it would be interesting to learn about it.
Question for discussion: It would be interesting to know about the tasks where you used Deferred / Promises.