- Creative-Area: http://www.creative-area.net
- jQuery Core:
$.ajax
,$.Deferred
, ... - W3C: XMLHttpRequest
- github: jaubourg
- www: http://jaubourg.net
- email: [email protected]
- twitter: @jaubourg
- Learn about Promises
- Rethink what you think you know about them
- Learn about
$.when
$.Deferred
then()
Oh and I gist-wrapped it for you (take that hipsters!)
THIS TALK IS NOT SEQUENTIAL
So sit back, relax, think asynchronously
ENJOY THE RIDE
- a means to observe a one-shot task
- an ajax request
- an upload
- an animation
- solving an equation
- ...
- two possible outcomes:
- positive: true / success / resolution
- negative: false / error / rejection
- (can notify progress along the way)
It's an Interface:
state()
-- "pending", "resolved", "rejected"done()
fail()
always()
progress()
then()
-- akapipe()
promise()
-- returns itself (WAAAAAT?!?)
The jqXHR object implements it:
var jqXHR = $.ajax( options );
jqXHR.done( function() {
console.log( "SUCCESS" );
} );
jqXHR.fail( function() {
console.log( "ERROR" );
} );
You can add callbacks at any time.
This is where the AWESOMENESS strikes btw
var jqXHR = $.ajax( options );
// before the request returns
jqXHR.done( function() {
console.log( "the request just returned" );
} );
// after the request returned
jqXHR.done( function() {
console.log( "called immediately" );
} );
Groovy!
var request = $.ajax( options );
request.then( function() {
console.log( "SUCCESS" );
}, function() {
console.log( "ERROR" );
} );
The word "then" is a dead giveaway:
- the promise is kind of an async boolean
- "then" is kind of an async conditional
var request = $.sjax( options );
if ( request.resolved() ) {
console.log( "SUCCESS" );
} else {
console.log( "ERROR" );
}
So done()
and fail()
are also conditionals?
request.done(
success
);
request.fail(
error
);
if ( request.resolved() ) {
success();
}
if ( !request.resolved() ) {
error();
}
- Promise => boolean
then()
,done()
,fail()
=> if/then/else, if, if not$.when()
=> logical AND
var template = $.ajax( "template" );
var data = $.ajax( "data" );
$.when( template, data ).then( function() {
console.log( "template + data available" );
} );
is roughly the same as
var template = $.sjax( "template" );
var data = $.sjax( "data" );
if ( template.resolved() && data.resolved() ) {
console.log( "we got both template and data" );
}
Notable differences:
- pure parallelism, no critical path, no sequence
$.when()
fails as soon as one argument fails
$.when( pending_1, rejected, pending_2 ).fail( function() {
// called immediately
} );
To mark an object as observable:
- give it a
promise()
method - have this method return a promise (duh!)
OMG, a Promise is observable through itself!
promise.promise() === promise; // true
Why use a method?
- dynamic behaviour
- one entry point vs the entire interface
See jQuery collections:
var $div = $( "div" );
$div.done( fn ); // errors
$div.fail( fn ); // errors
// etc
// But the following works
$div.promise().done( function() {
console.log( "all animations stopped" );
} );
And also on $.ready() (WAAAAAT?!?)
$.ready.promise(); // exists!
$.when() calls this internally!
$.when(
$.ajax( "data" ),
$.ajax( "template" ),
$.ready
).done( function() {
// populate template and insert it into the DOM
} );
- implements the Promise interface
- plus methods to manage state and callbacks
resolve()
reject()
notify()
$.Deferred()
:
- don't need no
new
- can take an init function (use it! use it! use it!)
function timeout( time ) {
return $.Deferred( function( defer ) {
setTimeout( defer.resolve, time );
} );
}
// ...
var afterTwentySecs = timeout( 20000 );
afterTwentySecs.done( function() {
console.log( "at least 20 seconds later" );
} );
// ... and 50 seconds later...
afterTwentySecs.done( function() {
console.log( "immediately" );
} );
But wait:
var afterTwentySecs = timeout( 20000 );
afterTwentySecs.done( function() {
console.log( "20 seconds later, right?" );
} );
afterTwentySecs.resolve(); // Guess not :(
Be safe, protect yourself, always use (return) a promise:
function timeout( time ) {
return $.Deferred( function( defer ) {
setTimeout( defer.resolve, time );
} ).promise();
}
timeout( 20000 ).resolve(); // throws
It's our fault really, a Deferred:
- should be Observable
- shouldn't be a Promise
Time to break the web again!
- some objects are Observable through a Promise
- a Promise is an Interface
- a Promise is a tri-state boolean
then()
,done()
andfail()
are conditionals$.when()
is a logical AND$.Deferred()
can be used to:- create a Promise
- control its state
How about some...
promise.done( fn1 ).fail( fn2 ).always( fn3 ) === promise; // true
var defer = Deferred();
var resolve = defer.resolve;
resolve( "hello" ); // does NOT throw
defer.done( function( string ) {
// called
string === "hello"; // true
} );
simplify your life:
// avoid
myFunctionWithACallback( function( arg ) {
defer.resolve( arg );
} );
// rather
myFunctionWithACallback( defer.resolve );
var defer = Deferred();
defer.done( function( number ) {
console.log( "called with " + number );
} );
defer.resolve( 1 );
defer.resolve( 2 );
$.when( "omg a string" ).done( function( string ) {
string === "omg a string"; // true
} );
Works when combined too
$.when( 1, Deferred().resolve( 2 ), 3 ).done( function( a, b, c ) {
a === 1; // true
b === 2; // true
c === 3; // true
} );
var defer1 = $.Deferred().resolve( 1, 2 );
var defer2 = $.Deferred().resolve( 3, 4 )
$.when( defer1, defer2 ).done( function( a, b ) {
// a => [ 1, 2 ]
// b => [ 3, 4 ]
} );
then()
returns a new Promise!
defer.then().promise() !== defer.promise();
you can filter values
Deferred().resolve( 3 ).then( function( number ) {
return number * 2;
} ).done( function( newNumber ) {
newNumber === 6; // true
} );
WYRIWYG: what you return is what you get
Deferred().resolve( 3 ).then( function( number ) {
// omg procrastination
} ).done( function( newNumber ) {
newNumber === undefined; // true
} );
you can chain tasks
timeout( 2000 ).then( function() {
return timeout( 3000 );
} ).done( function() {
// 5 seconds later!
} );
you can combine
timeout( 2000 ).then( function() {
return timeout( 3000 );
} ).then( function() {
return "whatever";
} ).done( function( string ) {
// 5 seconds later!
string === "whatever";
} );
even inception-style
timeout( 2000 ).then( function() {
return timeout( 3000 ).then( function() {
return "whatever";
} );
} ).done( function( string ) {
// 5 seconds later!
string === "whatever";
} );
you can switch states
Deferred().reject( "error" ).then( null, function() {
return Deferred().resolve( "whatever" );
} ).done( function( string ) {
string === "whatever"; // true
} );
you can even catch exceptions when and only when necessary:
Deferred().resolve( 0 ).then( function( number ) {
try {
// what could possibly go wrong?
return 1 / number;
} catch( e ) {
return Deferred().reject( e );
}
} );
How many Deferreds are created here ?
timeout( 2000 ).then( function() {
return timeout( 3000 );
} ).done( function() {
// 5 seconds later!
} );
function getUserData( id ) {
return ajax( "path/to/service", {
data: {
id: id
}
} ).then( null, function( jqXHR ) {
return jqXHR.status === 409 ? getUserData( id ) : jqXHR;
} );
}
var cache = {};
function getCachedUserData( id ) {
if ( !cache[ id ] ) {
cache[ id ] = getUserData( id );
}
return cache[ id ];
}
each id will only be requested once!
getCachedUserData( 750412 ); // put in cache
getCachedUserData( 750412 ); // taken from cache even if not complete
function getMultipleData() {
return $.when.apply( null, $.map( arguments, getCachedUserData ) );
}
getMultipleData( 860224, 750412, 500428, 860224 );
function _getMultipleData( ids ) {
return ajax( "path/to/service", {
data: {
ids: ids
}
} ).then( null, function( jqXHR ) {
return jqXHR.status === 409 ? _getMultipleData( ids ) : jqXHR;
} );
}
var nextRequest;
var idsForNextRequest;
function getUserData( id ) {
if ( !nextRequest ) {
idsForNextRequest = [];
nextRequest = timeout( 400 ).then( function() {
nextRequest = undefined;
return _getMultipleData( idsForNextRequest );
} );
}
idsForNextRequest.push( id );
return nextRequest.then( function( data ) {
return data[ id ];
} );
}
now, how many requests here?
getMultipleData( [ 860224, 750412, 500428, 860224 ] );
setTimeout( function() {
getMultipleData( [ 750412, 500428, 860224, 660712 ] );
}, 2000 );
talk.state() === "questions";
Fixed, thanks @seutje :)