.when() to use deferreds – Alex McPherson, Quick Left



.when() to use deferreds – Alex McPherson, Quick Left

0 0


reveal-deferreds

My Deferreds problem/solutions talk converted to reveal.js for jQuery Russia, 2013

On Github alexmcpherson / reveal-deferreds

.when() to use deferreds

Alex McPherson, Quick Left

Talk Outline

  • Who is Alex McPherson
  • History of deferreds
  • $.Deferred API walkthrough and demo
  • 6 Problems, 6 Solutions
  • Best Practices
  • Q & A

Alex McPherson

Important, crucial facts:

  • Sublime Text 2
  • 14pt Inconsolata Font
  • Railscasts 2 Color Theme
  • Commas last
  • Always, always, always include my semicolons

Alex McPherson

Boulder, Colorado

Alex McPherson

www.QuickLeft.com

History of Deferreds

  • November 2009, $.ajax requests
  • Julian Aubourg steps up, rewrites
  • Suggested that internal API be exposed
  • Reworked, released January 2010
  • ‘Based on’ CommonJS Promises/A proposal
  • Used internally in jQuery, AJAX and effects
  • How do we use it?

$.Deferred API

A deferred is a state storage object

var dfd = $.Deferred();

dfd.state(); // 'pending'

$.Deferred API

Three states possible:

  • pending
  • resolved
  • rejected

$.Deferred API

var dfd1 = $.Deferred(); // 'pending'
dfd1.reject();           // 'rejected'

dfd2 = $.Deferred();     // 'pending'
dfd2.resolve();          // 'resolved'

$.Deferred API

ONE WAY. Once you leave 'pending', there is no going back, and no changing your mind

Rejected is always rejected, and resolved is always resolved

$.Deferred API

Changing the state of something isn't that useful by itself, you could just set it as a variable

The killer feature of deferreds is that you can listen to state changes

$.Deferred API

dfd.done(function() {
  console.log('This deferred was -resolved-')
});

dfd.fail(function() {
  console.log('This deferred was -rejected-')
});

dfd.always(function() {
  console.log('This deferred was either rejected or resolved')
});

Calling .reject() or .resolve() will change the state, triggering a callback. This is similar to $.ajax and .success(), .error(), and .complete()

$.Deferred API

Other useful methods:

dfd.progress( fn )
dfd.notify() //triggers callback, no state change

dfd.then( resolveFn, rejectFn, notifyFn )
$.when( dfds* )
dfd.promise()
          

$.Deferred API

Summary:

.reject() --> .fail( fn ) --> .state() == 'rejected'

.resolve() --> .done( fn ) --> .state() == 'resolved'

.notify() --> .progress( fn ) --> .state() == 'pending'

Examples!

A deferred might not immediately appear useful

I will show a common task, and an elegant way to solve it using a deferred.

Example 1: Bundling Ajax Calls

Task: Wait for several ajax calls to complete before doing something

Potential solution:

$.ajax({
  success:
})

Example 1: Bundling Ajax Calls

Task: Wait for several ajax calls to complete before doing something

Potential solution:

$.ajax({
  success: function() {
    $.ajax({
      success: function() {

      }
    })
  }
})

Example 1: Bundling Ajax Calls

Task: Wait for several ajax calls to complete before doing something

Potential solution:

$.ajax({
  success: function() {
    $.ajax({
      success: function() {
        $.ajax({
          //PLEASE don't do this
        })
      }
    })
  }
})

Example 1: Bundling Ajax Calls

Another attempt, saving state externally:

var completedCalls = [];

$.ajax({
  success: function() {
    completedCalls.push('facebookData');
    checkAllCompleted();
  }
});

$.ajax({
  success: function() {
    completedCalls.push('twitterData');
    checkAllCompleted();
  }
});

var checkAllCompleted = function() {
  //Check completedCalls array for all 3 values
}

There's a better way!

Example 1: Bundling Ajax Calls

var getAddressData = function() {
  return $.ajax({
    //ajax code here
  })
};

Example 1: Bundling Ajax Calls

var getAddressData =  function() {...};
var getTwitterData =  function() {...};
var getFacebookData = function() {...};

//all 3 return promises (deferreds you can't reject/resolve yourself)

Example 1: Bundling Ajax Calls

var getAddressData =  function() {...};
var getTwitterData =  function() {...};
var getFacebookData = function() {...};

$.when(

  getAddressData(),
  getTwitterData(),
  getFacebookData()

).done(function() {
  // do something with all that data
})

Example 1: Bundling Ajax Calls

$.when accepts many promises or deferreds and represents them as one.

This means that if one is rejected, the whole 'bundle' is rejected immediately.

Example 2: The Template Pattern

Task: You are rendering different views on a single page application. Some need external data, others don't.

Example 2: The Template Pattern

var aboutPage = {
  init: function() {
    this.promise = this.getInfo() //ajax call
  }
}

Example 2: The Template Pattern

var aboutPage = {...}

var contactPage = {
  init: function() {
    // I don't need to fetch data here.
    // I want to provide the
    // same interface to my object.
  }
}

Example 2: The Template Pattern

var aboutPage = {...}

var contactPage = {
  init: function() {
    this.promise = $.Deferred().resolve();
  }
}

Example 2: The Template Pattern

var aboutPage = {...}

var contactPage = {...}

var changePage = function(page) {
  $.when(page.promise).done(function() {
    page.render();
  });
}

Example 2: The Template Pattern

var aboutPage = {...}

var contactPage = {
  init: function() {
    // this.promise = $.Deferred().resolve();
    // This does the same thing!
    // $.when(undefined) resolves immediately
  }
}

Example 2: The Template Pattern

$.when lets you treat any object that isn't a deferred like a deferred.

This lets you sometimes use deferreds, and other times not, but treat the objects in the same manner.

Example 3: Caching

Task: You have to minimize the number of API calls you make in an application

Example 3: Caching

var UserDetails = {
  cache: {},
  search: function(user_name) {
    if (!this.cache[user_name]) {
      this.cache[user_name] = $.getJSON('www.mysite.com/?q=' + user_name);
    }

    return this.cache[username];
  }
}

Example 3: Caching

UserDetails.search('bill_smith').done(function(results) {
  //made a round trip to the server
  console.log(results);
})

//After some usage:

UserDetails.cache.bill_smith //Promise from $.getJSON
UserDetails.cache.joe_taylor //Promise from $.getJSON

How is that useful?

Example 3: Caching

UserDetails.search('bill_smith').done(function(results) {
  // Did NOT make a round trip to the server this time
  // Deferred passes the same params from the original callback
  console.log(results);
})

Example 3: Caching

This trick counts on the fact that you resolve a deferred with parameters, and that those get passed to any callback attached to the same promise

This lets you store both the state of a request (failed, resolved) as well as the data it returned in the same object.

Example 4: Readable, maintainable javascript

Task: Write literate, pretty javascript

Understandable code is maintainable code

setTimout(function() {
  flashBanner();
}, 500);

Example 4: Readable, maintainable javascript

$.wait = function(duration) {
  // The deferred constructor can take an argument
  return $.Deferred(function(def) {
    setTimeout(def.resolve, duration);
  });
}

Example 4: Readable, maintainable javascript

//Pretty! Readable!
$.wait(500).then(flashBanner);

Example 4: Readable, maintainable javascript

Another example of readable, maintainable code:

$.when(
  doSearch('twitter'), // These must return promises
  doSearch('email'),
  doSearch('weather')
).done(function(twitter, email, weather) {

  renderHomePage(twitter, email, weather);

});

Example 4: Readable, maintainable javascript

Defereds have already done a good job having natural-language syntax, but you can extend that

Write methods that return promises!

Example 5: Progress and notify

You have a hotel booking site with long searches. Keep your user updated!

Example 5: Progress and notify

Your search takes 3 steps: Geocoding, hotel search, and pricing search.

geoCode('Moscow'); //returns locale_id
hotelSearch(421124); //returns array of hotels
priceSearch([23, 343, 6653, 23463]); //returns prices by hotel

Example 5: Progress and notify

How can you update a progress meter if it takes 20 seconds to search?

var searchDfd = $.Deferred();

searchDfd.progress( updateProgressBar );
searchDfd.done( showResultsPage );

geoCode('Moscow', searchDfd);
// When this completes, it calls the next search
// as well as searchDfd.notify('geocodeComplete');

Example 5: Progress and notify

Each stage of the search calls .notify() as it completes, and the final search calls .resolve() to finish the search.

Demo!

Example 6: Managing Animations

How can you orchestrate many simultaneous animations to make sure they run on time?

  • .animate()
  • .fadeIn()
  • .fadeOut()
  • .hide()
  • .show()
  • .slideUp()
  • .slideDown()

These all attach a promise to their DOM node

Example 6: Managing Animations

How can you orchestrate many simultaneous animations to make sure they run on time?

var myBanner = $('h1');

myBanner.hide(2000);

myBanner.promise().done(function() {
  console.log("It's disappeared!");
})

Example 6: Managing Animations

How can you orchestrate many simultaneous animations to make sure they run on time?

var complexRemove = function() {
  var defs = [];

  defs.push( $('h1').delay(Math.random()*5000).fadeOut() );
  defs.push( $('div').fadeIn(100).delay(200).slideDown() );

  //makes an array of defferds one deferred
  return $.when.apply(null, defs);
}

complexRemove().done(function() {
  window.close();
});

Example 6: Managing Animations

Syncing multiple animations can be hard withuot deferreds.

Call node.promise() to start working with them

Other use cases

  • Registering multiple callbacks!
  • Post-resolution binding
  • Dependent $.ajax calls
  • Memoizing expensive operations
  • Implementing a publisher/subscriber pattern

Take home lessons for deferreds:

  • When in doubt, return a deferred
  • Better yet, return a promise
  • They are powerful when stored as a property on objects
  • They can make your code prettier
  • They can make your life easier!

What we covered:

  • Bundling ajax calls together
  • Normalizing by using .done on non-deferreds
  • Making a cache object
  • Prettier syntax and DSLs for maintainable code
  • Progress on a long running search using .notify
  • Managing complicated animations

Спасибо!

  • @alexmcpherson
  • github.com/alexmcpherson
  • quickleft.com