In this article, we explore the concepts of Deferred and Promise in JavaScript. They are a very important feature in JavaScript toolkits (such as Dojo and MochiKit) and have recently made their debut in the popular JavaScript library jQuery (already in version 1.5 matter). Deferred provides an abstract non-blocking solution (such as a response to an Ajax request) by creating a "promise" object whose purpose is to return a response at some point in the future. If you haven’t come across “promises” before, we’ll cover them in more detail below.
Abstractly speaking, deferreds can be understood as a way to represent time-consuming operations that take a long time to complete. Compared with blocking functions, they are asynchronous, rather than blocking the application to wait for it to complete and then return. result. The deferred object will return immediately, and then you can bind callback functions to the deferred object, and they will be called after the asynchronous processing is completed.
Promise
You may have read some information about the implementation details of promises and deferreds. In this chapter, we briefly introduce how promises work, which is applicable to almost all JavaScript frameworks that support deferreds.
In general, promises serve as a model that provides a solution for describing the concept of delay (or future) in software engineering. The idea behind it has already been introduced: instead of executing a method and then blocking the application waiting for the result to be returned, a promise object is returned to satisfy the future value.
An example will help to understand. Suppose you are building a web application that relies heavily on data from a third-party API. 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 the result. Deferreds provide a better solution to this problem, being non-blocking and completely decoupled from the code.
Promise/A proposal 'defines a 'then' method to register a callback, which will be executed when the handler function returns the result. The pseudo code for it to return a promise looks like this:
promise = callToAPI( arg1, arg2, ...);
promise.then(function( futureValue ) {
/* handle futureValue */
});
promise.then(function( futureValue ) {
/* do something else */
});
In addition, the promise callback will be executed in the following two different states:
•resolved: In this case, the data is available
•rejected: In this case, an error occurred and no value is available
Fortunately, the 'then' method accepts two parameters : One for when the promise is resolved (resolved) and the other for when the promise is rejected (rejected). Let's go back to the pseudo code:
promise. then( function( futureValue ) {
/* we got a value */
} , function() {
/* something went wrong */
} );
In some cases, we need to get multiple results returned before continuing the application (for example, displaying a dynamic set of 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 execution can continue only after all promises are satisfied.
when(
promise1,
promise2,
...
).then(function( futureValue1, futureValue2, ... ) {
/* all promises have completed and are resolved */
});
A good example is a scenario where you might have multiple animations running at the same time. Without tracking the callback after each animation execution, it is difficult to perform the next task after the animation is completed. However, using promises and 'when' methods can be very straightforward: once the animation is completed, the next task can be performed. The final result is that we can simply use a callback to solve the problem of waiting for multiple animation execution results. For example:
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 */
});
这意味着,基本上可以用非阻塞的逻辑方式编写代码并异步执行。 而不是直接将回调传递给函数,这可能会导致紧耦合的接口,通过promise模式可以很容易区分同步和异步的概念。
在下一节中,我们将着眼于jQuery实现的deferreds,你可能会发现它明显比现在所看到的promise模式要简单。
jQuery的Deferreds jQuery在1.5版本中首次引入了deferreds。它 所实现的方法与我们之前描述的抽象的概念没有大的差别。原则上,你获得了在未来某个时候得到‘延时'返回值的能力。在此之前是无法单独使用的。 Deferreds 作为对ajax模块较大重写的一部分添加进来,它遵循了CommonJS的promise/ A设计。1.5和先前的版本包含deferred功能,可以使$.ajax() 接收调用完成及请求出错的回调,但却存在严重的耦合。开发人员通常会使用其他库或工具包来处理延迟任务。新版本的jQuery提供了一些增强的方式来管理 回调,提供更加灵活的方式建立回调,而不用关心原始的回调是否已经触发。 同时值得注意的是,jQuery的递延对象支持多个回调绑定多个任务,任务本身可以既可以是同步也可以是异步的。
您可以浏览下表中的递延功能,有助于了解哪些功能是你需要的:
jQuery.Deferred() |
创建一个新的Deferred对象的构造函数,可以带一个可选的函数参数,它会在构造完成后被调用。 |
jQuery.when() |
通过该方式来执行基于一个或多个表示异步任务的对象上的回调函数 |
jQuery.ajax() |
执行异步Ajax请求,返回实现了promise接口的jqXHR对象 |
deferred.then(resolveCallback,rejectCallback) |
添加处理程序被调用时,递延对象得到解决或者拒绝的回调。 |
deferred.done() |
当延迟成功时调用一个函数或者数组函数.
|
deferred.fail() |
当延迟失败时调用一个函数或者数组函数.。
|
deferred.resolve(ARG1,ARG2,...) |
调用Deferred对象注册的‘done'回调函数并传递参数 |
deferred.resolveWith(context,args) |
调用Deferred对象注册的‘done'回调函数并传递参数和设置回调上下文 |
deferred.isResolved |
确定一个Deferred对象是否已经解决。 |
deferred.reject(arg1,arg2,...) |
调用Deferred对象注册的‘fail'回调函数并传递参数 |
deferred.rejectWith(context,args) |
调用Deferred对象注册的‘fail'回调函数并传递参数和设置回调上下文 |
deferred.promise() |
返回promise对象,这是一个伪造的deferred对象:它基于deferred并且不能改变状态所以可以被安全的传递 |
The core of jQuery delay implementation is jQuery.Deferred: a constructor that can be called in a chain. ... It should be noted that the default state of any deferred object is unresolved, and the callback will be added to the queue through the .then() or .fail() method and executed later in the process.
The following example of $.when() accepting multiple parameters
function successFunc(){ console.log( “success!” ); }
function failureFunc(){ console.log( “failure!” ); }
$. when(
$.ajax( "/main.php" ),
$.ajax( "/modules.php" ),
$.ajax( "/lists.php" )
) .then( successFunc, failureFunc );
What’s interesting in the implementation of $.when() is that it can not only parse deferred objects, but also pass parameters that are not deferred objects. When processing, they will be treated as deferred objects and callbacks (doneCallbacks) will be executed immediately. This is also worth mentioning in jQuery's Deferred implementation. In addition, deferred.then() also provides support for the deferred.done and deferred.fail() methods to add callbacks to the deferred's queue.
Using the deferred function mentioned in the table introduced earlier, let’s look at a code example. Here we create a very basic application: get (1) an external news source via the $.get method (which returns a promise) and (2) get the latest reply. At the same time, the program also implements the animation of the news and reply content display container through the function (prepareInterface()).
To ensure that the above three steps are completed before executing other related actions, we use $.when(). The .then() and .fail() handlers can be used to perform other program logic depending on your needs.
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( 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!” );
});
deferreds behind the scenes of ajax Use in operations does not mean they cannot be used elsewhere. In this section, we'll see some solutions where using deferreds will help abstract away asynchronous behavior and decouple our code.
Asynchronous Caching When it comes to asynchronous tasks, caching can be a bit demanding because you have to ensure that the task is only executed once for the same key. Therefore, the code needs to track inbound tasks somehow. For example, the following code snippet:
$.cachedGetScript ( url, callback1 );
$.cachedGetScript( url, callback2 );
The caching mechanism needs to ensure that the script can only be requested once, regardless of whether it already exists in the cache. Therefore, in order for the caching system to handle the request correctly, we ultimately need to write some logic to track the callbacks bound to a given url.
Thankfully, this is exactly the kind of logic deferred implements, so we can do it like this:
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 pretty simple: we cache a promise object for each URL. If the given url does not have a promise, we create a deferred and make the request. If it already exists we just need to bind a callback to it. A big advantage of this solution is that it handles new and cached requests transparently. Another advantage is that a deferred-based cache handles failure situations gracefully. When the promise ends with the 'rejected' status, we can provide an error callback to test:
$.cachedGetScript( url ).then( successCallback, errorCallback );
Please remember: no matter Whether the request is cached or not, the above code snippet will work normally!
Universal asynchronous cache In order to make the code as general as possible, we build a cache factory and abstract out the tasks that actually need to be performed:
$.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 rewrite cachedGetScript:
$.cachedGetScript = $.createCache(function( defer, url ) {
$.getScript( url ).then( defer.resolve, defer.reject );
});
Each call to createCache will create a new cache library and return a new cache retrieval function. Now we have a generic cache factory that makes it easy to implement logical scenarios involving getting values from the cache.
Image loading Another candidate scenario is image loading: make sure we don't load the same image twice, we may need to load the image. It's easy to implement using createCache:
$.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:
$.loadImage( "my-image.png" ).done( callback1 );
$.loadImage( "my-image.png" " ).done( callback2 );
Caching will work normally regardless of whether image.png has been loaded or is in the process of being loaded.
Cache data for API responses API requests which are considered immutable during the life cycle of your page are also perfect candidates for caching. For example, perform the following operations:
$.searchTwitter = $. createCache(function( defer, query ) {
$.ajax({
url: "http://search.twitter.com/search.json",
data: { q: query },
dataType: "jsonp",
success: defer.resolve,
error: defer.reject
});
});
Program Allows you to perform searches on Twitter while caching them:
$.searchTwitter( "jQuery Deferred", callback1 );
$.searchTwitter( "jQuery Deferred", callback2 );
Timing Deferred-based caching is not limited to network requests; it can also be used for timing purposes.
For example, you may need to perform an action after a given period of time on the web page to attract the user's attention to a specific feature that is not easily noticed or to deal with a delay issue. While setTimeout is suitable for most use cases, it provides no solution after the timer has fired or even theoretically expired. We can use the following caching system to handle it:
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 );
}
});
});
New afterDOMReady helper method provides domReady with minimal counters appropriate time later. If the delay has expired, the callback will be executed immediately.
Synchronize multiple animations Animation is another common example of asynchronous tasks. However executing the code after several unrelated animations have completed is still a bit challenging. Although the function of obtaining promise objects on animated elements is only provided in jQuery 1.6, it is easy to implement manually:
$.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 the different Animation:
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 trick and create some Auxiliary method:
$.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 helper code as follows to synchronize the animation:
$.when(
$( "#div1" ).fadeOutPromise(),
$( "#div2" ).fadeInPromise ( "fast" )
).done(function() {
/* both animations are done */
});
One-time event While jQuery provides all the time binding methods you could possibly need, things can get a little tricky when events only need to be handled once. (Unlike $.one() )
For example, you might want to have a button that opens a panel when it is first clicked, and then performs specific initialization logic after the panel is opened. When dealing with this situation, people usually write code like this:
var buttonClicked = false;
$( "#myButton" ).click(function() {
if ( !buttonClicked ) {
buttonClicked = true;
initializeData();
showPanel();
}
});
Soon, you may add some actions when the button is clicked after the panel is opened, as follows:
if ( buttonClicked ) { /* perform specific action */ }
This is a very coupled solution. If you want to add some other operations, you must edit the binding code or make a copy. If you don't, your only option is to test buttonClicked. Since buttonClicked may be false, the new code may never be executed and therefore you may lose this new action.
Using deferreds we can do better (for simplicity, the code below will only apply to a single element and a single event type, but it can be easily extended to multiple event types Collection):
$.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 if the element has a deferred object bound to a given event
• If not, create it so that it fires the event in the first place Solve
• Then bind the given callback on the deferred and return the promise
The code is verbose, but it will simplify the processing of related issues. Let us first define a helper method:
$ .fn.firstClick = function( callback ) {
return this.bindOnce( "click", callback );
};
Then, the previous logic can be reconstructed As follows:
var openPanel = $( "# myButton" ).firstClick();
openPanel.done( initializeData );
openPanel.done( showPanel );
If we need to perform some actions, only when the panel After opening, all we need is this:
openPanel .done(function() { /* perform specific action */ });
If the panel is not open, the action will be delayed until the button is clicked.
Composition Assistant Looking at each of the above examples individually, the role of promise is limited. However, the real power of promises is mixing them together.
Loads the panel content and opens the panel on first click Suppose, we have a button that opens a panel, requests its content and then fades in the content. Using the helper method we defined earlier, we can do this:
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 Suppose, we already have the panel with content, but we only want it when the button is clicked for the first time Loads images and fades in when all images have loaded successfully. The HTML code is as follows:
We use the data-src attribute to describe the image real path. Then the code to use the promise assistant to solve this use case 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 keep track of all LoadImage promises , then add the panel slideDown animation. So the first time the button is clicked, the panel will slideDown and the images will start loading. The panel will only fade in once you've finished sliding it down and all images have been loaded.
Load images on the page after a specific delay Suppose, we want to implement deferred image display on the entire page. To do this, the format of the HTML we need is as follows:
The meaning is very simple:
•image1.png, the third image is displayed immediately , the first image will be displayed after one second
·image2.png, the second image will be displayed after one second, and the fourth image will be displayed after two seconds
How will we achieve this?
$( "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 lazy load the image itself, the code will be different:
$( "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 met before trying to load the image. This makes sense when you want to limit the number of network requests while a page is loading.
Conclusion As you can see, promises are very useful even without Ajax requests. By using the deferred implementation in jQuery 1.5, it is very easy to separate asynchronous tasks from your code. This way, you can easily separate logic from your application.