Browse files

Merge pull request #156 from johnkpaul/promises_content

add deferreds content
  • Loading branch information...
2 parents 0711375 + 76defa7 commit da12e4da8ed36f88ccda691361f8c4daa0016e33 @addyosmani addyosmani committed Dec 14, 2012
View
3 order.yml
@@ -92,6 +92,7 @@
- feature-browser-detection
- deferreds:
- about-deferreds
- - something-else
+ - jquery-deferreds
+ - examples
- jquery-mobile:
- getting-started
View
114 page/code-organization/deferreds.md
@@ -0,0 +1,114 @@
+---
+title: Deferreds
+level: advanced
+source: http://msdn.microsoft.com/en-us/magazine/gg723713.aspx
+attribution:
+ - Julian Aubourg <j@ubourg.net>
+ - Addy Osmani <addyosmani@gmail.com>
+ - Andree Hansson <peolanha@gmail.com>
+---
+
+At a high-level, deferreds can be thought of as a way to represent
+asynchronous operations which can take a long time to complete. They're the
+asynchronous alternative to blocking functions and the general idea is
+that rather than your application blocking while it awaits some request
+to complete before returning a result, a deferred object can instead be
+returned immediately. You can then attach callbacks to the deferred
+object: they will be called once the request has actually completed.
+
+##Promises
+
+In its most basic form, a 'promise' is a model that provides a solution
+for the concept of deferred (or future) results in software engineering.
+The main idea behind it is something we've already covered: rather than
+executing a call which may result in blocking, we instead return a
+promise for a future value that will eventually be satisfied.
+
+If it helps to have an example here, consider that you are building a
+web application which heavily relies on data from a third party API. A
+common problem that's faced is having an unknown knowledge of the API
+server's latency at a given time so it's possible that other parts of
+your application may be blocked from running until a result from it is
+returned. Deferreds provide a better solution to this problem, one which
+is void of 'blocking' effects and completely decoupled.
+
+The [Promise/A](http://wiki.commonjs.org/wiki/Promises/A) proposal
+defines a method called 'then' that can be used to register callbacks to
+a promise and, thus, get the future result when it is available. The
+pseudo-code for dealing with a third party API that returns a promise
+may look like:
+
+```
+promise = callToAPI( arg1, arg2, ...);
+
+promise.then(function( futureValue ) {
+ /* handle futureValue */
+});
+
+promise.then(function( futureValue ) {
+ /* do something else */
+});
+```
+
+Furthermore, a promise can actually end up being in two different
+states:
+
+- resolved: in which case data is available
+- rejected: in which case something went wrong and no value is
+ available
+
+Thankfully, the 'then' method accepts two parameters: one for when the
+promise was resolved, another for when the promise was rejected. If we
+get back to pseudo-code, we may do things like:
+
+```
+promise.then( function( futureValue ) {
+ /* we got a value */
+} , function() {
+ /* something went wrong */
+} );
+```
+
+In the case of certain applications, it is necessary to have several
+results returned before your application can continue at all (for
+example, displaying a dynamic set of options on a screen before a user
+is able to select the option that interests them).Where this is the
+case, a method called 'when' exists, which can be used to perform some
+action once all the promises have been fully fulfilled:
+
+```
+when(
+ promise1,
+ promise2,
+ ...
+).then(function( futureValue1, futureValue2, ... ) {
+ /* all promises have completed and are resolved */
+});
+```
+
+A good example is a scenario where you may have multiple concurrent
+animations that are being run. Without keeping track of each callback
+firing on completion, it can be difficult to truly establish once all
+your animations have finished running. Using promises and 'when' however
+this is very straightforward as each of your animations can effectively
+say 'we promise to let you know once we're done'. The compounded result
+of this means it's a trivial process to execute a single callback once
+the animations are done. For example:
+
+```
+var promise1 = $("#id1").animate().promise();
+var promise2 = $("#id2").animate().promise();
+when(
+ promise1,
+ promise2
+).then(function(){
+ /* once both animations have completed
+ we can then run our additional logic */
+});
+```
+
+This means that one can basically write non-blocking logic that can be
+executed without synchronization. Rather than directly passing callbacks
+to functions, something which can lead to tightly coupled interfaces,
+using promises allows one to separate concerns for code that is
+synchronous or asynchronous.
View
427 page/code-organization/deferreds/examples.md
@@ -0,0 +1,427 @@
+---
+title: Deferreds
+level: advanced
+source: http://msdn.microsoft.com/en-us/magazine/gg723713.aspx
+attribution:
+ - Julian Aubourg <j@ubourg.net>
+ - Addy Osmani <addyosmani@gmail.com>
+ - Andree Hansson <peolanha@gmail.com>
+---
+
+##Further Deferreds examples
+
+Deferreds are used behind the hood in Ajax but it doesn't mean they can't also
+be used elsewhere. This section describes situations where deferreds will help
+abstract away asynchronous behaviour and decouple our code.
+
+### Caching
+
+#### Asynchronous cache
+
+When it comes to asynchronous tasks, caching can be a bit demanding
+since you have to make sure a task is only performed once for a given
+key. As a consequence, the code has to somehow keep track of inbound
+tasks.
+
+```
+$.cachedGetScript( url, callback1 );
+$.cachedGetScript( url, callback2 );
+```
+
+The caching mechanism has to make sure the url is only requested once
+even if the script isn't in cache yet. This shows some logic
+to keep track of callbacks bound to a given url in order for the cache
+system to properly handle both complete and inbound requests.
+
+```
+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 );
+};
+```
+
+One promise is cached per url. If there is no promise for the given url yet,
+then a deferred is created and the request is issued. If it already exists, however,
+the callback is attached to the existing deferred. The big advantage of this
+solution is that it will handle both complete and inbound requests
+transparently. Another advantage is that a deferred-based cache will deal with
+failure gracefully. The promise will end up rejected which can be tested for by
+providing an error callback:
+
+```
+$.cachedGetScript( url ).then( successCallback, errorCallback );
+```
+
+#### Generic asynchronous cache
+
+It is also possible to make the code completely generic and build a
+cache factory that will abstract out the actual task to be performed
+when a key isn't in the cache yet:
+
+```
+$.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 request logic is abstracted away, cachedGetScript can be rewritten
+as follows:
+
+```
+$.cachedGetScript = $.createCache(function( defer, url ) {
+ $.getScript( url ).then( defer.resolve, defer.reject );
+});
+```
+
+This will work because every call to createCache will create a new cache
+repository and return a new cache-retrieval function.
+
+#### Image loading
+
+A cache can be used to ensure that the same image is not loaded multiple times.
+
+```
+$.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;
+});
+```
+
+Again, the following snippet:
+
+```
+$.loadImage( "my-image.png" ).done( callback1 );
+$.loadImage( "my-image.png" ).done( callback2 );
+```
+
+will work regardless of whether my-image.png has already been loaded or
+not, or if it is actually in the process of being loaded.
+
+#### Caching Data API responses
+
+API requests that are considered immutable during the lifetime of your
+page are also perfect candidates. For instance, the following:
+
+```
+$.searchTwitter = $.createCache(function( defer, query ) {
+ $.ajax({
+ url: "http://search.twitter.com/search.json",
+ data: {
+ q: query
+ },
+ dataType: "jsonp",
+ success: defer.resolve,
+ error: defer.reject
+ });
+});
+```
+
+will allow you to perform searches on Twitter and cache them at the same
+time:
+
+```
+$.searchTwitter( "jQuery Deferred", callback1 );
+$.searchTwitter( "jQuery Deferred", callback2 );
+```
+
+#### Timing
+
+This deferred-based cache is not limited to network requests; it can
+also be used for timing purposes.
+
+For instance, you may need to perform an action on the page after a
+given amount of time so as to attract the user's attention to a specific
+feature they may not be aware of or deal with a timeout (for a quiz
+question for instance). While setTimeout is good for most use-cases it
+doesn't handle the situation when the timer is asked for later, even
+after it has theoretically expired. We can handle that with the
+following caching system:
+
+```
+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 proper timing after the DOM
+is ready while ensuring the bare minimum of timers will be used. If the
+delay is already expired, any callback will be called right away.
+
+### One-time event
+
+While jQuery offers all the event binding one may need, it can become a
+bit cumbersome to handle events that are only supposed to be dealt with
+once.
+
+For instance, you may wish to have a button that will open a panel the
+first time it is clicked and leave it open afterwards or take special
+initialization actions the first time said button is clicked. When
+dealing with such a situation, one usually end up with code like this:
+
+```
+var buttonClicked = false;
+
+$( "#myButton" ).click(function() {
+ if ( !buttonClicked ) {
+ buttonClicked = true;
+ initializeData();
+ showPanel();
+ }
+});
+```
+
+then, later on, you may wish to take actions, but only if the panel is
+opened:
+
+```
+if ( buttonClicked ) {
+ /* perform specific action */
+}
+```
+
+This is a very coupled solution. If you want to add some other action,
+you have to edit the bind code or just duplicate it all. If you don't,
+your only option is to test for buttonClicked and you may lose that new
+action because the buttonClicked variable may be false and your new code
+may never be executed.
+
+We can do much better using deferreds (for simplification sake, the
+following code will only work for a single element and a single event
+type, but it can be easily generalized for full-fledged collections with
+multiple event types):
+
+```
+$.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 already has a deferred attached for the given
+ event
+- if not, create it and make it so it is resolved when the event is
+ fired the first time around
+- then attach the given callback to the deferred and return the
+ promise
+
+While the code is definitely more verbose, it makes dealing with the
+problem at hand much simpler in a compartmentalized and decoupled way.
+But let's define a helper method first:
+
+```
+$.fn.firstClick = function( callback ) {
+ return this.bindOnce( "click", callback );
+};
+```
+
+Then the logic can be re-factored as follows:
+
+```
+var openPanel = $( "#myButton" ).firstClick();
+
+openPanel.done( initializeData );
+openPanel.done( showPanel );
+```
+
+If an action should be performed only when a panel is opened later on:
+
+```
+openPanel.done(function() {
+ /* perform specific action */
+});
+```
+
+Nothing is lost if the panel isn't opened yet, the action will just get
+deferred until the button is clicked.
+
+### Combining helpers
+
+All of the samples above can seem a bit limited when looked at
+separately. However, the true power of promises comes into play when you
+mix them together.
+
+#### Requesting panel content on first click and opening said panel
+
+Following is the code for a button that, when clicked, opens a panel.
+It requests its content over the wire and then fades the content in. Using
+the helpers defined earlier, it could be defined as:
+
+```
+$( "#myButton" ).firstClick(function() {
+ var panel = $( "#myPanel" );
+ $.when(
+ $.get( "panel.html" ),
+ panel.slideDownPromise()
+ ).done(function( ajaxResponse ) {
+ panel.html( ajaxResponse[ 0 ] ).fadeIn();
+ });
+});
+```
+
+#### Loading images in a panel on first click and opening said panel
+
+Another possible goal is to have the panel fade in, only after the button
+has been clicked and after all of the images have been loaded.
+
+The html code for this would look something like:
+
+```
+<div id="myPanel">
+ <img data-src="image1.png" />
+ <img data-src="image2.png" />
+ <img data-src="image3.png" />
+ <img data-src="image4.png" />
+</div>
+```
+
+We use the data-src attribute to keep track of the real image location.
+The code to handle our use case using our promise helpers 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 the loadImage promises. We later
+join them with the panel slideDown animation using $.when. So when the
+button is first clicked, the panel will slideDown and the images will
+start loading. Once the panel has finished sliding down and all the
+images have been loaded, then, and only then, will the panel fade in.
+
+#### Loading images on the page after a specific delay
+
+In order to implement deferred image display on the entire page,
+the following format in HTML can be used.
+
+```
+ <img data-src="image1.png" data-after="1000" src="placeholder.png" />
+ <img data-src="image2.png" data-after="1000" src="placeholder.png" />
+ <img data-src="image1.png" src="placeholder.png" />
+ <img data-src="image2.png" data-after="2000" src="placeholder.png" />
+```
+
+What it says is pretty straight-forward:
+
+- load image1.png and show it immediately for the third image and
+ after one second for the first one
+- load image2.png and show it after one second for the second image
+ and after two seconds for the fourth image
+
+
+```
+$( "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();
+ });
+ }
+});
+```
+
+In order to delay the loading of the images themselves:
+
+```
+$( "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, after the delay to be fulfilled then the image is loaded. It can make a
+lot of sense when you want to limit the number or network requests on page
+load.
View
112 page/code-organization/deferreds/jquery-deferreds.md
@@ -0,0 +1,112 @@
+---
+title: Deferreds
+level: advanced
+source: http://msdn.microsoft.com/en-us/magazine/gg723713.aspx
+attribution:
+ - Julian Aubourg <j@ubourg.net>
+ - Addy Osmani <addyosmani@gmail.com>
+ - Andree Hansson <peolanha@gmail.com>
+---
+
+##jQuery Deferreds
+
+Deferreds were added as a part of a large rewrite of the ajax module,
+led by Julian Auborg following the CommonJS Promises/A design. Whilst 1.5 and
+above include deferred capabilities, former versions of jQuery had
+jQuery.ajax() accept callbacks that would be invoked upon completion or
+error of the request, but suffered from heavy coupling - the same
+principle that would drive developers using other languages or toolkits
+to opt for deferred execution.
+
+In practice what jQuery's version provides you with are several
+enhancements to the way callbacks are managed, giving you significantly
+more flexible ways to provide callbacks that can be invoked whether the
+original callback dispatch has already fired or not. It is also worth
+noting that jQuery's Deferred object supports having multiple callbacks
+bound to the outcome of particular tasks (and not just one) where the
+task itself can either be synchronous or asynchronous.
+
+At the heart of jQuery's implementation is jQuery.Deferred - a chainable
+constructor which is able to create new deferred objects that can check
+for the existence of a promise to establish whether the object can be
+observed. It can also invoke callback queues and pass on the success of
+synchronous and asynchronous functions. It's quite essential to note
+that the default state of any Deferred object is unresolved. Callbacks
+which may be added to it through .then() or .fail() are queued up and get
+executed later on in the process.
+
+You are able to use Deferred objects in conjunction with the promise concept of
+when(), implemented in jQuery as $.when() to wait for all of the Deferred
+object's requests to complete executing (ie. for all of the promises to be
+fulfilled). In technical terms, $.when() is effectively a way to execute
+callbacks based on any number of promises that represent asynchronous events.
+
+An example of $.when() accepting multiple arguments can be seen below in
+conjunction with .then():
+
+```
+function successFunc(){
+ console.log( "success!" );
+}
+
+function failureFunc(){
+ console.log( "failure!" );
+}
+
+$.when(
+ $.ajax( "/main.php" ),
+ $.ajax( "/modules.php" ),
+ $.ajax( "/lists.php" )
+).then( successFunc, failureFunc );
+```
+
+The $.when() implementation offered in jQuery is quite interesting as it not
+only interprets deferred objects, but when passed arguments that are not
+deferreds, it treats these as if they were resolved deferreds and executes any
+callbacks (doneCallbacks) right away. It is also worth noting that jQuery's
+deferred implementation, in addition to exposing deferred.then(), a jQuery
+promise also supports the deferred.done() and deferred.fail() methods which can
+also be used to add callbacks to the deferred's queues.
+
+We will now take a look at a code example that utilizes many of the deferred
+features mentioned in the table presented earlier. Here is a very basic
+application that consumes (1) an external news feed and (2) a reactions feed
+for pulling in the latest comments via $.get() (which will return a promise).
+The application also has a function (prepareInterface()) which returns a
+promise to complete animating our containers for both the news and
+reactions.
+
+
+```
+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!" );
+});
+```

0 comments on commit da12e4d

Please sign in to comment.