Skip to content

Instantly share code, notes, and snippets.

@jaubourg
Last active July 17, 2019 15:00
Show Gist options
  • Save jaubourg/6525351 to your computer and use it in GitHub Desktop.
Save jaubourg/6525351 to your computer and use it in GitHub Desktop.

stop procrastinating,

start $.Deferring!

Julian Aubourg

stuff I do

contact

goal of this talk

  • 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!)

warning

THIS TALK IS NOT SEQUENTIAL

So sit back, relax, think asynchronously

ENJOY THE RIDE

Promise

  • 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() -- aka pipe()
  • 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!

then()

(& the boolean metaphor)

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();
}

$.when()

  • 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
} );

Observable

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

} );

Deferred

  • 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!

Let's catch our breath

  • some objects are Observable through a Promise
  • a Promise is an Interface
  • a Promise is a tri-state boolean
  • then(), done() and fail() are conditionals
  • $.when() is a logical AND
  • $.Deferred() can be used to:
    • create a Promise
    • control its state

How about some...

implementation details

chaining().chaining().chaining()

promise.done( fn1 ).fail( fn2 ).always( fn3 ) === promise; // true

detachable methods

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 );

our methods are safe

var defer = Deferred();

defer.done( function( number ) {
	console.log( "called with " + number );
} );

defer.resolve( 1 );
defer.resolve( 2 );

$.when() with non-Observables

$.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
} );

multi-values and $.when()

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 ]
} );

get more advanced with then()

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 );

	}

} );

then() is expensive!!!

How many Deferreds are created here ?

timeout( 2000 ).then( function() {

	return timeout( 3000 );

} ).done( function() {

	// 5 seconds later!

} );

in the wild

retry on 409 - Conflict

function getUserData( id ) {
	return ajax( "path/to/service", {
		data: {
			id: id
		}
	} ).then( null, function( jqXHR ) {

		return jqXHR.status === 409 ? getUserData( id ) : jqXHR;

	} );
}

cache it

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

get multiple data at once

function getMultipleData() {
	return $.when.apply( null, $.map( arguments, getCachedUserData ) );
}

getMultipleData( 860224, 750412, 500428, 860224 );

make it optimal

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 );

Conclusion?

talk.state() === "questions";
@jaubourg
Copy link
Author

Fixed, thanks @seutje :)

@gfranko
Copy link

gfranko commented Feb 28, 2014

This is fantastic. Thanks for making this and presenting!

@staabm
Copy link

staabm commented Mar 10, 2014

$.Deferred() can take an init function (use it! use it! use it!)

great tip! saves you from creating crazy named variables ;-). thanks a lot!

@staabm
Copy link

staabm commented Mar 11, 2014

@jaubourg

how to promote rejection from an nested promise into outer promise?
Is it always required to manually do things like

inner.fail(function() {
  outer.reject();
});

or is there a smarter way to make this "dependency" clear..? ATM it feels like one could easily "break" a promise chain because of forgotten state-promotion of an inner promise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment