In this article, we explore the concepts of Deferred and Promise in JavaScript, a very important feature of JavaScript toolkits (such as Dojo and MochiKit), and have recently been unveiled in the popular JavaScript library for the first time. JQuery (already a version of the 1.5 thing). Deferred provides an abstract non-blocking solution, such as a response to an AJAX request, that creates a "promise" object that returns a response at some point in the future. If you have not contacted "promise" before, we will do a detailed description below.
In abstraction, Deferreds can be understood as a way of representing time-consuming operations that take a long time to complete, rather than blocking the application waiting for it to complete and then returning the result. The deferred object returns immediately, and then you can bind the callback function to the deferred object, which is invoked after the asynchronous process completes.
Promise
You may have read some information about promise and deferreds implementation details. In this section, we outline how the promise works, which are applicable in almost all JavaScript frameworks that support Deferreds.
In general, promise as a model provides a solution to describe the concept of delay (or future) in software engineering. The idea behind it is that instead of executing a method and blocking the application waiting for the result to return, it returns a Promise object to meet future values.
An example would help to understand that if you are building a Web application, it relies heavily on data from Third-party APIs. Then there is a common problem: we cannot learn the latency of an API response, and other parts of the application may be blocked until it returns results. Deferreds provides a better solution to the problem, which is non-blocking and completely decoupled from the code.
PROMISE/A proposed ' defines a ' then ' method to register the callback, which is executed when the processing function returns the result. It returns a promise pseudo code that looks like this:
Copy Code code 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 the following two different states:
resolved: In this case, the data is available
rejected: In this case, an error occurred and no values were available
Fortunately, the ' then ' method accepts two parameters: one for promise to be resolved (resolved) and the other for promise rejection (rejected). Let's go back to pseudocode:
Copy Code code as follows:
Promise.then (function (FutureValue) {
/* We got a value * *
}, function () {
/* Something went wrong * *
} );
In some cases, we need to get multiple return results, and then continue executing the application (for example, a set of dynamic options before the user can select the option they are interested in). In this case, the ' when ' method can be used to solve the scenario where all the promise are satisfied before they can continue.
Copy Code code as follows:
When
PROMISE1,
Promise2,
...
). Then (function (futureValue1, futureValue2, ...) {
* All promises have completed and are resolved * *
});
A good example of this is a scenario where you may have multiple animations running at the same time. If you do not track callbacks after each animation completes, it is difficult to do the next task after the animation completes. However, using the promise and ' when ' approach can be quite straightforward: once the animation is done, you can perform the next task. The end result is that we can simply use a callback to solve the problem of waiting for multiple animation execution results. For example:
Copy Code code 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 you can basically write code in a non-blocking logical way and execute it asynchronously. Rather than passing the callback directly to the function, this may result in a tightly coupled interface that can easily distinguish between synchronous and asynchronous concepts through promise patterns.
In the next section, we will look at the deferreds of the jquery implementation, and you may find that it is significantly simpler than the promise pattern now seen.
the deferreds of jquery
jquery introduced deferreds for the first time in version 1.5. The method it implements is not much different from the abstract concept we described earlier. In principle, you get the ability to get a ' deferred ' return value at some point in the future. It cannot be used alone until then. Deferreds is added as part of a larger rewrite of the Ajax module, which follows the promise/a design of the COMMONJS. The 1.5 and previous versions contain the deferred feature, which enables $.ajax () to receive callbacks that call completion and request errors, but with serious coupling. Developers typically use other libraries or toolkits to handle deferred tasks. The new version of jquery provides some enhanced ways to manage callbacks, providing a more flexible way to build callbacks without having to worry about whether the original callback was triggered. It is also noteworthy that the recursive object of jquery supports multiple callbacks to bind multiple tasks, and the task itself can be either synchronous or asynchronous.
You can explore the deferred features in the following table to help you understand which features you need:
Jquery.deferred () |
Creates a new deferred object's constructor, which can take an optional function parameter, which is invoked after the construction completes. |
Jquery.when () |
This way to perform a callback function on one or more objects that represent asynchronous tasks |
Jquery.ajax () |
Executes an asynchronous AJAX request, returning a JQXHR object that implements the Promise interface |
Deferred.then (Resolvecallback,rejectcallback) |
A callback that is resolved or rejected by a deferred object when a handler is invoked. |
Deferred.done () |
Call a function or array function when the delay succeeds. |
Deferred.fail () |
Call a function or array function when the delay fails ... |
Deferred.resolve (ARG1,ARG2, ...) ) |
Call the ' Done ' callback function registered by the deferred object and pass the argument |
Deferred.resolvewith (Context,args) |
Invokes the ' done ' callback function registered by the deferred object and passes the arguments and sets the callback context |
Deferred.isresolved |
Determines whether a deferred object has been resolved. |
Deferred.reject (ARG1,ARG2, ...) ) |
Call the ' fail ' callback function registered by the deferred object and pass the argument |
Deferred.rejectwith (Context,args) |
Calls the ' fail ' callback function registered by the deferred object and passes the arguments and sets the callback context |
Deferred.promise () |
Returns the Promise object, which is a forged deferred object: It is based on deferred and cannot change state so it can be safely delivered. |
The core of the jquery latency implementation is jquery.deferred: a constructor that can be chained to the call. ...... Note that the default state of any deferred object is unresolved, and the callback is added to the queue through the. then () or. Fail () method, and is executed later in the process.
Here's an example of a $.when () that accepts multiple parameters
Copy Code code 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);
Interestingly in the implementation of $.when (), it is not only possible to parse deferred objects, but also to pass parameters that are not deferred objects, and treat them as deferred objects and immediately execute callbacks (donecallbacks). This is also worth mentioning in the deferred implementation of jquery, in addition, Deferred.then () also provides support for the Deferred.done and Deferred.fail () methods to increase callbacks in deferred queues.
With the deferred features mentioned in the table described earlier, let's look at a code example. Here we create a very basic application: get an external news source (1) by $.get method (return a Promise) and (2) get the latest reply. The program also implements the animation of the news and reply content display container through functions (Prepareinterface ()).
To ensure that the above three steps are complete before performing other related actions, we use $.when (). Depending on your needs. then () and. Fail () processing functions can be used to perform other program logic.
Copy Code code as follows:
function Getlatestnews () {
Return $.get ("latestnews.php", function (data) {
Console.log ("News data received");
$ (". News"). HTML (data);
} );
}
function Getlatestreactions () {
Return $.get ("latestreactions.php", function (data) {
Console.log ("Reactions data received");
$ (". Reactions"). HTML (data);
} );
}
function Prepareinterface () {
Return $. Deferred (function (DFD) {
var latest = $ (". News,. Reactions");
Latest.slidedown (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!");
});
The use of deferreds in the behind-the-scenes operation of Ajax does not mean that they cannot be used elsewhere. In this section, we'll see that in some solutions, using deferreds will help abstract out asynchronous behavior and decouple our code.
Asynchronous Caching
When it comes to asynchronous tasks, caching can be a bit harsh because you have to make sure that the same key task is executed only once. Therefore, your code needs to track inbound tasks in some way. For example, the following code fragment:
Copy Code code as follows:
$.cachedgetscript (URL, callback1);
$.cachedgetscript (URL, callback2);
Caching mechanisms need to ensure that scripts are only requested once, regardless of whether they already exist in the cache. Therefore, in order for the caching system to handle the request correctly, we eventually need to write some logic to track the callbacks that are bound to the given URL.
Thankfully, this happens to be the kind of logic that deferred implements, so we can do this:
Copy Code code 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 fairly simple: we cache a Promise object for each URL. If the given URL does not promise, we create a deferred and issue the request. If it already exists we just need to bind the callback for it. One of the great advantages of the solution is that it transparently handles new and cached requests. Another advantage is that a deferred cache gracefully handles failures. When promise ends with a ' rejected ' state, we can provide an error callback to test:
$.cachedgetscript (URL). Then (Successcallback, Errorcallback);
Keep in mind that the code snippet above will work regardless of whether the request is cached or not!
Universal Asynchronous Caching
To make the code as generic as possible, we set up a cache factory and abstract the tasks that actually need to be performed:
Copy Code code 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 that the specific request logic has been abstracted, we can write back the cachedgetscript:
Copy Code code as follows:
$.cachedgetscript = $.createcache (function (defer, URL) {
$.getscript (URL). Then (Defer.resolve, Defer.reject);
});
Each time you call Createcache, a new cache library is created and a new cached retrieval function is returned. Now we have a common cache factory that can easily implement a logical scenario that involves taking a value from the cache.
Picture Loading
Another candidate scenario is image loading: Make sure we don't load the same image two times, we may need to load the image. Using Createcache is easy to achieve:
Copy Code code 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 next code snippet is as follows:
Copy Code code as follows:
$.loadimage ("My-image.png"). Done (CALLBACK1);
$.loadimage ("My-image.png"). Done (Callback2);
The cache works regardless of whether the image.png has been loaded or is in the process of loading.
API response to cached data
Which of your pages is considered immutable in the lifecycle of the API request, is also the cache perfect candidate scenario. For example, perform the following actions:
Copy Code code 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 while caching them:
Copy Code code as follows:
$.searchtwitter ("JQuery Deferred", callback1);
$.searchtwitter ("JQuery Deferred", Callback2);
timed
Deferred based caching is not limited to network requests; it can also be used for timed purposes.
For example, you might want to perform an action on a Web page for a period of time, to attract users ' attention to a particular feature that is not easily noticed, or to deal with a latency problem. Although settimeout is suitable for most use cases, the solution cannot be provided after the timer is set off and even theoretically expires. We can use the following caching system to handle:
Copy Code code 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 helper method provides the right time after domready with the fewest counters. If the delay has expired, the callback will be executed immediately.
Synchronizing multiple animations
Animations are another common example of asynchronous tasks. However, it is still a bit challenging to execute code after several unrelated animations have been completed. Although the ability to get promise objects on an animated element is provided in jQuery1.6, it is easy to manually implement:
Copy Code code 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 synchronize different animations using $.when ():
Copy Code code as follows:
var fadediv1out = $ ("#div1"). Animatepromise ({opacity:0}),
fadediv2in = $ ("#div1"). Animatepromise ({opacity:1}, "fast");
$.when (Fadediv1out, fadediv2in). Done (function () {
/* Both animations ended * *
});
We can also use the same techniques to create a number of helper methods:
Copy Code code 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 you can use the new helper code to synchronize the animation as follows:
Copy Code code as follows:
$.when (
$ ("#div1"). Fadeoutpromise (),
$ ("#div2"). Fadeinpromise ("Fast")
). Done (function () {
/* Both animations are done * *
});
One-off event
While jquery provides all the time-binding methods you might need, the situation can become a bit tricky when the event needs to be handled only once. (unlike $.one ())
For example, you might want to have a button that opens a panel when it is first clicked, and then executes a specific initialization logic after the panel is opened. When dealing with this situation, people usually write code like this:
Copy Code code as follows:
var buttonclicked = false;
$ ("#myButton"). Click (function () {
if (!buttonclicked) {
ButtonClicked = true;
Initializedata ();
Showpanel ();
}
});
After a while, you may be able to add some action when you click on the button after the panel is open, as follows:
Copy Code code as follows:
if (buttonclicked) {/* Perform specific action */}
This is a very coupled solution. If you want to add some other action, you must edit the binding code or copy one. If you don't, your only option is to test buttonclicked. Since buttonclicked may be false, the new code may never be executed, so you may lose the new action.
We can do better with deferreds (for simplicity, the following code applies only to a single element and a single event type, but it can easily be extended to a collection of multiple event types):
Copy Code code 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 that the element is already bound to a deferred object for a given event
• If not, create it so that it is resolved at the first time of triggering the event
• Then bind the given callback to the deferred and return to promise
Although the code is verbose, it simplifies the handling of related issues. Let's first define an auxiliary method:
Copy Code code as follows:
$.fn.firstclick = function (callback) {
Return This.bindonce ("click", Callback);
};
The previous logic can then be refactored as follows:
Copy Code code as follows:
var Openpanel = $ ("#myButton"). Firstclick ();
Openpanel.done (initializedata);
Openpanel.done (Showpanel);
If we need to do some action, only when the panel is open, all we need is this:
Copy Code code as follows:
Openpanel.done (function () {/* Perform specific action */});
If the panel is not open, the action is delayed until the button is clicked.
Portfolio Assistant
Looking at each of these examples alone, promise's role is limited. However, the real power of promise is to mix them together.
Load Panel contents and open panel on first click
If we have a button, we can open a panel, request its contents, and fade into the content. Using the helper method we defined earlier, we can do this:
Copy Code code 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 on the first click
If we already have content on the panel, we only want to load the image the first time the button is clicked and fade in the image when all the images are loaded successfully. The HTML code is as follows:
Copy Code code as follows:
<div id= "Mypanel" >
</div>
We use the Data-src property to describe the true path of the picture. The code that uses the Promise helper to resolve the use case is as follows:
Copy Code code 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 the LoadImage promise, and then add the panel Slidedown animation. So the first time you click the button, the panel will slidedown and the image will start to load. The panel fades in as soon as you complete the slide down panel and all the images that have been loaded.
load the image on the page after a specific delay
Suppose we want to implement a deferred image display throughout the page. To do this, the HTML format we need is as follows:
Copy Code code as follows:
The meaning is very simple:
Image1.png, the third image is immediately displayed, one second after the first image display
Image2.png second image after one second, two seconds to display fourth image
How will we achieve it?
Copy Code code 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 the loading of the image itself, the code will be different:
Copy Code code 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 for the delay condition to be satisfied before attempting to load the picture. It makes sense to limit the number of network requests when you want to load a page.
Conclusions
As you can see, promise is useful even in the absence of Ajax requests. By using the deferred implementation in jquery 1.5, it is very easy to isolate asynchronous tasks from your code. In this way, you can easily detach logic from your application.