Next we will explore the concepts of Deferred and Promise in JavaScript. Deferred provides an abstract non-blocking solution (such as Ajax request response), which creates a promise object, the purpose is to return a response at a certain point in the future. If you are interested, refer to this Article. Let's explore the concepts of Deferred and Promise in JavaScript, they are a very important feature in the JavaScript Toolkit (such as Dojo and MochiKit), and have recently made their debut in the popular JavaScript library jQuery (which is already a version 1.5 issue ). Deferred provides an abstract non-blocking solution (such as Ajax Request Response). It creates a "promise" object to return a response at a certain time point in the future. If you have never touched "promise" before, we will introduce it in detail below.
Abstract: deferreds can be understood as a way to express time-consuming operations that take a long time to complete. Compared with blocking functions, deferreds are asynchronous, instead of blocking the application and waiting for its completion and then returning results. The deferred object will return immediately, and then you can bind the callback function to the deferred object, which will be called after asynchronous processing.
Promise
You may have read more about the implementation details of promise and deferreds. In this section, we will give a general introduction to how promise works, which are applicable to almost all javascript frameworks that support deferreds.
Generally, as a model, promise provides a solution that describes the concept of latency (or future) in software engineering. We have already introduced the idea behind it: instead of executing a method and blocking the application to wait for the result to return, it returns a promise object to meet future values.
For example, it is helpful to understand that, if you are building a web application, it depends heavily on data from third-party APIs. Then we will face a common problem: we cannot know the delay time of an API response, and other parts of the application may be blocked until it returns results. Deferreds provides a better solution to this problem, which is non-blocking and completely decoupled from the code.
Promise/A proposes to define A 'then' method to register the callback. The callback will be executed when the processing function returns the result. It returns a promise pseudo code that looks like this:
The Code is as follows:
Promise = callToAPI (arg1, arg2 ,...);
Promise. then (function (futureValue ){
/* Handle futureValue */
});
Promise. then (function (futureValue ){
/* Do something else */
});
In addition, the promise callback is executed in two different States:
• Resolved: in this case, data is available
• Rejected: In this case, an error occurs and no value is available.
Fortunately, the 'then' method accepts two parameters: one for promise to be resolved (resolved), and the other for promise to reject (rejected ). Let's go back to the pseudocode:
The Code is as follows:
Promise. then (function (futureValue ){
/* We got a value */
}, Function (){
/* Something went wrong */
});
In some cases, we need to obtain multiple returned results before executing the application (for example, before users can select the options they are interested in, displays a set of dynamic options ). In this case, the 'when' method can be used to solve the scenario where all promise is satisfied before execution can continue.
The Code is as follows:
When (
Promise1,
Promise2,
...
). Then (function (futureValue1, futureValue2 ,...){
/* All promises have completed and are resolved */
});
A good example is that you may have multiple running animations at the same time. If you do not track the callback after each animation is executed, it is difficult to execute the next task after the animation is completed. However, the use of promise and 'when' can be very straightforward: Once the animation is completed, you can execute the next task. The final result is that we can simply use a callback to solve the problem of waiting for multiple animation execution results. For example:
The Code is as follows:
When (function (){
/* Animation 1 */
/* Return promise 1 */
}, Function (){
/* Animation 2 */
/* Return promise 2 */
}). Then (function (){
/* Once both animations have completed we can then run our additional logic */
});
This means that code can be written in non-blocking logic and executed asynchronously. Instead of passing the callback directly to the function, this may lead to tightly coupled interfaces. Through the promise mode, you can easily distinguish between synchronous and asynchronous concepts.
In the next section, we will focus on the deferreds implemented by jQuery. You may find it much simpler than the current promise mode.
Deferreds of jQuery
JQuery introduced deferreds for the first time in version 1.5. The methods it implements are no different from the abstract concepts we described earlier. In principle, you will be able to get the 'delay' return value at some point in the future. Before that, it cannot be used independently. Deferreds is added as A part of the large rewrite of the ajax module. It complies with the promise/A design of CommonJS. 1.5 and earlier versions include the deferred function, which allows $. ajax () to receive calls completed and request error callbacks, but has severe coupling. Developers usually use other libraries or toolkit to process delayed tasks. The new version of jQuery provides some enhanced methods to manage callbacks and provides a more flexible way to establish callbacks without worrying about whether the original callback has been triggered. At the same time, it is worth noting that jQuery's deferred object supports multiple callbacks to bind multiple tasks. The task itself can be both synchronous and asynchronous.
You can browse the deferred functions shown in the following table to find out which functions are required:
JQuery. Deferred () |
Create a new Deferred object constructor with an optional function parameter that will be called after the constructor is constructed. |
JQuery. when () |
This method is used to execute callback functions based on one or more objects that represent asynchronous tasks. |
JQuery. ajax () |
Executes asynchronous Ajax requests and returns the jqXHR object that implements the promise interface. |
Deferred. then (resolveCallback, rejectCallback) |
When the added handler is called, the deferred object is resolved or rejected. |
Deferred. done () |
Call a function or array function when the delay is successful. |
Deferred. fail () |
Call a function or array function when the delay fails .. |
Deferred. resolve (ARG1, ARG2 ,...) |
Call the 'done' callback function registered with the Deferred object and pass the Parameter |
Deferred. resolveWith (context, args) |
Call the 'done' callback function registered with the Deferred object and PASS Parameters and set the callback context. |
Deferred. isResolved |
Determine whether a Deferred object has been resolved. |
Deferred. reject (arg1, arg2 ,...) |
Call the 'failed' callback function registered with the Deferred object and pass the Parameter |
Deferred. rejectWith (context, args) |
Call the 'failed' callback function registered with the Deferred object and PASS Parameters and set the callback context. |
Deferred. promise () |
Returns the promise object, which is a forged deferred object. It is based on deferred and cannot change the state, so it can be safely transmitted. |
JQuery. Deferred: A constructor that can be called in a chain ....... Note that the default status of any deferred object is unresolved. The callback will be added to the queue through the. then () or. fail () method and executed later.
The following $. when () accepts multiple parameters:
The Code is as follows:
Function successFunc () {console. log ("success !" );}
Function failureFunc () {console. log ("failure !" );}
$. When (
$. Ajax ("/main. php "),
$. Ajax ("/modules. php "),
$. Ajax ("/lists. php ")
). Then (successFunc, failureFunc );
At $. in the implementation of when (), it is interesting that it can not only parse the deferred object, but also pass parameters that are not a deferred object, during Processing, they are treated as deferred objects and are immediately executed for callback (doneCallbacks ). This is also a place to mention in the Deferred implementation of jQuery. In addition, deferred. then () is also deferred. done and deferred. the fail () method adds callback support to the deferred queue.
Using the deferred function mentioned in the preceding table, let's look at a sample code. Here, we create a very basic application: get an external news source (1) through the $. get method (returns a promise) and (2) get the latest reply. At the same time, the program uses the function (prepareInterface () to display the container animation for news and reply content.
We use $. when () to ensure that the preceding three steps are completed before other related behaviors are executed (). According to your needs, the. then () and. fail () processing functions can be used to execute other program logic.
The Code is as follows:
Function getLatestNews (){
Return $. get ("latestNews. php", function (data ){
Console. log ("news data received ed ");
$ (". News" pai.html (data );
});
}
Function getLatestReactions (){
Return $. get ("latestReactions. php", function (data ){
Console. log ("reactions data already ed ");
$ (". Reactions" example .html (data );
});
}
Function prepareInterface (){
Return $. Deferred (function (dfd ){
Var latest = $ (". news,. reactions ");
Latest. slideDown (500, dfd. resolve );
Latest. addClass ("active ");
}). Promise ();
}
$. When (
GetLatestNews (), getLatestReactions (), prepareInterface ()
). Then (function (){
Console. log ("fire after requests succeed ");
}). Fail (function (){
Console. log ("something went wrong !" );
});
Using deferreds in ajax behind-the-scenes operations does not mean they cannot be used elsewhere. In this section, we will see that in some solutions, using deferreds will help abstract asynchronous behavior and decouple our code.
Asynchronous Cache
When asynchronous tasks are involved, caching can be a bit harsh, because you must ensure that the same key task is executed only once. Therefore, the code needs to track inbound tasks in some way. For example, the following code snippet:
The Code is as follows:
$. CachedGetScript (url, callback1 );
$. CachedGetScript (url, callback2 );
The cache mechanism must ensure that the script can be requested only once, whether or not it already exists in the cache. Therefore, in order for the cache system to correctly process requests, we need to write some logic to track the callback bound to the given url.
Fortunately, this is exactly the logic implemented by deferred, so we can do this:
The Code is as follows:
Var cachedScriptPromises = {};
$. CachedGetScript = function (url, callback ){
If (! CachedScriptPromises [url]) {
CachedScriptPromises [url] = $. Deferred (function (defer ){
$. GetScript (url). then (defer. resolve, defer. reject );
}). Promise ();
}
Return cachedScriptPromises [url]. done (callback );
};
The code is quite simple: We cache a promise object for each url. If the given url does not have promise, we create a deferred and send a request. If it already exists, we only need to bind a callback for it. A major advantage of this solution is that it is transparent in handling new and cached requests. Another advantage is that a deferred-based Cache gracefully handles failures. When promise ends in the 'objected' state, we can provide an error callback for testing:
$. CachedGetScript (url). then (successCallback, errorCallback );
Remember: The above code segment will work normally no matter whether the request has been cached or not!
General asynchronous Cache
To make the code as generic as possible, we create a cache factory and abstract the actual tasks to be executed:
The Code is as follows:
$. CreateCache = function (requestFunction ){
Var cache = {};
Return function (key, callback ){
If (! Cache [key]) {
Cache [key] = $. Deferred (function (defer ){
RequestFunction (defer, key );
}). Promise ();
}
Return cache [key]. done (callback );
};
}
Now the specific request logic has been abstracted. We can re-write cachedGetScript:
The Code is as follows:
$. CachedGetScript = $. createCache (function (defer, url ){
$. GetScript (url). then (defer. resolve, defer. reject );
});
Each time createCache is called, a new cache library is created and a new cache retrieval function is returned. Now we have a general cache factory, which can easily implement logic scenarios involving values from the cache.
Image loading
Another candidate scenario is image loading: ensure that we do not load the same image twice, and we may need to load the image. CreateCache is easy to implement:
The Code is as follows:
$. 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 );
};
Image. onerror = defer. reject;
Image. src = url;
});
The following code snippet is as follows:
The Code is as follows:
$. LoadImage ("my-image.png"). done (callback1 );
$. LoadImage ("my-image.png"). done (callback2 );
No matter whether image.png has been loaded or is being loaded, the cache will work normally.
API response of cached data
Which API requests are considered immutable during the lifecycle of your page are also perfect candidates for caching. For example, perform the following operations:
The Code is as follows:
$. SearchTwitter = $. createCache (function (defer, query ){
$. Ajax ({
Url: "http://search.twitter.com/search.json ",
Data: {q: query },
DataType: "jsonp ",
Success: defer. resolve,
Error: defer. reject
});
});
The program allows you to search on Twitter and cache them at the same time:
The Code is as follows:
$. SearchTwitter ("jQuery Deferred", callback1 );
$. SearchTwitter ("jQuery Deferred", callback2 );
Timing
The deferred-based Cache is not limited to network requests. It can also be used for scheduled purposes.
For example, you may need to execute an action after a specified period of time on the webpage to attract users to pay attention to a specific function that is not easy to notice or to handle a latency problem. Although setTimeout is suitable for most use cases, it cannot provide a solution after the timer starts or even theoretically expires. We can use the following cache system for processing:
The Code is as follows:
Var readyTime;
$ (Function () {readyTime = jQuery. now ();});
$. AfterDOMReady = $. createCache (function (defer, delay ){
Delay = delay | 0;
$ (Function (){
Var delta = $. now ()-readyTime;
If (delta >= delay) {defer. resolve ();}
Else {
SetTimeout (defer. resolve, delay-delta );
}
});
});
The new afterDOMReady auxiliary method provides the appropriate time after domReady with the least counter. If the delay has expired, the callback will be executed immediately.
Synchronize multiple animations
Animation is another common asynchronous task example. However, it is still a bit challenging to execute code after several irrelevant animations are completed. Although jQuery1.6 only provides the ability to obtain promise objects on animation elements, it is easy to implement manually:
The Code is as follows:
$. Fn. animatePromise = function (prop, speed, easing, callback ){
Var elements = this;
Return $. Deferred (function (defer ){
Elements. animate (prop, speed, easing, function (){
Defer. resolve ();
If (callback ){
Callback. apply (this, arguments );
}
});
}). Promise ();
};
Then, we can use $. when () to synchronize different animations:
The Code is as follows:
Var fadeDiv1Out = $ ("# p1"). animatePromise ({opacity: 0 }),
FadeDiv2In = $ ("# p1"). animatePromise ({opacity: 1}, "fast ");
$. When (fadeDiv1Out, fadeDiv2In). done (function (){
/* Both animations ended */
});
We can also use the same technique to establish some auxiliary methods:
The Code is as follows:
$. Each (["slideDown", "slideUp", "slideToggle", "fadeIn", "fadeOut", "fadeToggle"],
Function (_, name ){
$. Fn [name + "Promise"] = function (speed, easing, callback ){
Var elements = this;
Return $. Deferred (function (defer ){
Elements [name] (speed, easing, function (){
Defer. resolve ();
If (callback ){
Callback. apply (this, arguments );
}
});
}). Promise ();
};
});
Then I want to use the new assistant code to synchronize the animation as follows:
The Code is as follows:
$. When (
$ ("# P1"). fadeOutPromise (),
$ ("# P2"). fadeInPromise ("fast ")
). Done (function (){
/* Both animations are done */
});
One-time event
Although jQuery provides all the time binding methods you may need, it may be tricky to handle events only once. (Unlike $. one)
For example, you may want a button to open a panel when it is clicked for the first time. After the panel is opened, a specific initialization logic is executed. When dealing with this situation, people usually write code like this:
The Code is as follows:
Var buttonClicked = false;
$ ("# MyButton"). click (function (){
If (! ButtonClicked ){
ButtonClicked = true;
InitializeData ();
ShowPanel ();
}
});
Soon after, you may add some operations when you click the button after opening the Panel, as shown below:
The Code is as follows:
If (buttonClicked) {/* perform specific action */}
This is a very coupled solution. If you want to add other operations, you must edit the binding code or copy one copy. If you do not, your only choice is to test buttonClicked. Because buttonClicked may be false, new code may never be executed, so you may lose this new action.
We can do better with deferreds (for simplicity, the following code will only apply to a single element and a single event type, but it can easily expand to multiple event types ):
The Code is as follows:
$. Fn. bindOnce = function (event, callback ){
Var element = $ (this [0]),
Defer = element. data ("bind_once_defer _" + event );
If (! Defer ){
Defer = $. Deferred ();
Function deferCallback (){
Element. unbind (event, deferCallback );
Defer. resolveWith (this, arguments );
}
Element. bind (event, deferCallback)
Element. data ("bind_once_defer _" + event, defer );
}
Return defer. done (callback). promise ();
};
The code works as follows:
• Check whether the element has been bound to a deferred object for a given event
• If not, create it so that it can be resolved immediately after the event is triggered.
• Then bind the given callback on the deferred and return the promise
Although the Code is lengthy, it simplifies the handling of related problems. Let's first define an auxiliary method:
The Code is as follows:
$. Fn. firstClick = function (callback ){
Return this. bindOnce ("click", callback );
};
Then, the previous logic can be reconstructed as follows:
The Code is as follows:
Var openPanel = $ ("# myButton"). firstClick ();
OpenPanel. done (initializeData );
OpenPanel. done (showPanel );
If we need to perform some actions, only after the Panel is opened, what we need is as follows:
The Code is as follows:
OpenPanel. done (function () {/* perform specific action */});
If the Panel is not opened, the action will be delayed until you click the button.
Combined Assistant
Looking at each of the above examples, the role of promise is limited. However, the real power of promise is to mix them together.
Load the panel content and open the panel when you click it for the first time.
If we have a button, we can open a panel, request its content, and then fade in the content. Using the Helper method we defined earlier, we can do this:
The Code is as follows:
Var panel = $ ("# myPanel ");
Panel. firstClick (function (){
$. When (
$. Get ("panel.html "),
Panel. slideDownPromise ()
). Done (function (ajaxResponse ){
Panel.html (ajaxResponse [0]). fadeIn ();
});
});
Load the image and open the panel when you click it for the first time.
If we already have the panel content, we only want to load the image when the button is clicked for the first time and fade the image after all the images are loaded successfully. The HTML code is as follows:
The Code is as follows:
We use the data-src attribute to describe the true path of the image. The code for solving this case using promise assistant is as follows:
The Code is as follows:
$ ("# MyButton"). firstClick (function (){
Var panel = $ ("# myPanel "),
Promises = [];
$ ("Img", panel). each (function (){
Var image = $ (this), src = element. attr ("data-src ");
If (src ){
Promises. push (
$. LoadImage (src). then (function (){
Image. attr ("src", src );
}, Function (){
Image. attr ("src", "error.png ");
})
);
}
});
Promises. push (panel. slideDownPromise ());
$. When. apply (null, promises). done (function () {panel. fadeIn ();});
});
The trick here is to track all LoadImage promise, and then add the Panel slideDown animation. Therefore, when you click the button for the first time, the Panel will slideDown and the image will start loading. After sliding down the Panel and loading all the images, the Panel fades in.
Attach images on the page after a specific latency
Suppose we want to display the deferred image on the entire page. To do this, we need the following HTML format:
The Code is as follows:
The meaning is very simple:
• Image1.png: The third image is displayed immediately. The first image is displayed one second later.
• Image2.png displays the second image in one second and the fourth image in two seconds
How will we implement it?
The Code is as follows:
$ ("Img"). each (function (){
Var element = $ (this ),
Src = element. attr ("data-src "),
After = element. attr ("data-after ");
If (src ){
$. When (
$. LoadImage (src ),
$. AfterDOMReady (after)
). Then (function (){
Element. attr ("src", src );
}, Function (){
Element. attr ("src", "error.png ");
}). Done (function (){
Element. fadeIn ();
});
}
});
If we want to delay loading the image itself, the code will be different:
The Code is as follows:
$ ("Img"). each (function (){
Var element = $ (this ),
Src = element. attr ("data-src "),
After = element. attr ("data-after ");
If (src ){
$. AfterDOMReady (after, function (){
$. LoadImage (src). then (function (){
Element. attr ("src", src );
}, Function (){
Element. attr ("src", "error.png ");
}). Done (function (){
Element. fadeIn ();
});
});
}
});
Here, we first wait until the latency conditions are met before trying to load the image. When you want to load a page, it makes sense to specify the number of network requests.
Conclusion
As you can see, promise is useful even if there is no Ajax request. By using deferred in jQuery 1.5, it is very easy to separate asynchronous tasks from your code. In this way, you can easily separate the logic from your application.