Upload
trevorburnham
View
27.964
Download
1
Embed Size (px)
DESCRIPTION
Talk given at HTML5DevConf on April 1, 2013.
Citation preview
Sane Async PatternsTrevor Burnham
HTML5DevConf 2013
In This Talk
• Callback arguments considered harmful
• Three alternative patterns:
• PubSub
• Promises
• AMD
The Callback ArgumentAnti-Pattern
Pyramid of DoommainWindow.menu("File", function(err, file) { if(err) throw err; file.openMenu(function(err, menu) { if(err) throw err; menu.item("Open", function(err, item) { if(err) throw err; item.click(function(err) { if(err) throw err; window.createDialog('DOOM!', function(err, dialog) { if(err) throw err; ... }); }); }); }); });
A JS-er’s Lament
// Synchronous version of previous slidetry { var file = mainWindow.menu("File"); var menu = file.openMenu(); var item = menu.item("Open"); item.click() window.createDialog('DOOM!');} catch (err) { ...}
A Silver Lining
myFunction1();// No state changes here!myFunction2();
// Which means we never have to do this...while (!document.ready) { Thread.sleep(0);}
Mo’ Threads...
Nested SpaghettimainWindow.menu("File", function(err, file) { if(err) throw err; file.openMenu(function(err, menu) { if(err) throw err; menu.item("Open", function(err, item) { if(err) throw err; item.click(function(err) { if(err) throw err; window.createDialog('DOOM!', function(err, dialog) { if(err) throw err; ... }); }); }); }); });
Inflexible APIsfunction launchRocketAt(target, callback) { var rocket = {x: 0, y: 0}, step = 0;
function moveRocket() { rocket.x += target.x * (step / 10); rocket.y += target.y * (step / 10); drawSprite(rocket); if (step === 10) { callback(); } else { step += 1; setTimeout(moveRocket, 50); } }
moveRocket();}
Inflexible APIs
launchRocketAt(target, function() { // OK, so the rocket reached its target...});
Pattern I: PubSub
What is PubSub?
button.on("click", function(event) { ...});
server.on("request", function(req, res, next) { ...});
model.on("change", function() { ...});
What is PubSub for?
• Just about everything!
• When in doubt, use PubSub
How to use it?
• Pick a PubSub library, such ashttps://github.com/Wolfy87/EventEmitter
• If you’re on Node, you already have one
• Simply make your objects inherit from EventEmitter, and trigger events on them
An Evented RocketRocket.prototype.launchAt = function(target) { rocket = this; _.extend(rocket, {x: 0, y: 0, step: 0});
function moveRocket() { // Physics calculations go here... if (rocket.step === 10) { rocket.emit('complete', rocket); } else { rock.step += 1; setTimeout(moveRocket, 50); } rocket.emit('moved', rocket); }
rocket.emit('launched', rocket); moveRocket(); return this;}
An Evented Rocket
var rocket = new Rocket();rocket.launchAt(target).on('complete', function() { // Now it’s obvious what this callback is!});
PubSub Drawbacks
• No standard
• Consider using LucidJS:https://github.com/RobertWHurst/LucidJS
Pattern II: Promises
What is a Promise?
• “A promise represents the eventual value returned from the single completion of an operation.”—The Promises/A Spec
What is a Promise?
• An object that emits an event when an async task completes (or fails)
Pending
Resolved
Rejected
Example 1: Ajax
var fetchingData = $.get('myData');fetchingData.done(onSuccess);fetchingData.fail(onFailure);fetchingData.state(); // 'pending'
// Additional listeners can be added at any timefetchingData.done(celebrate);
// `then` is syntactic sugar for done + failfetchingData.then(huzzah, alas);
Example 2: Effects
$('#header').fadeTo('fast', 0.5).slideUp('fast');$('#content').fadeIn('slow');var animating = $('#header, #content').promise();
animating.done(function() { // All of the animations started when promise() // was called are now complete.});
What is a Promise?
• “A promise is a container for an as-yet-unknown value, and then’s job is to extract the value out of the promise”
http://blog.jcoglan.com/2013/03/30/callbacks-are-imperative-promises-are-functional-nodes-biggest-missed-opportunity/
Making Promises
// A Promise is a read-only copy of a Deferredvar deferred = $.Deferred();asyncRead(function(err, data) { if (err) { deferred.reject(); } else { deferred.resolve(data); };});var Promise = deferred.promise();
Without Promises$.fn.loadAndShowContent(function(options) { var $el = this; function successHandler(content) { $el.html(content); options.success(content); } function errorHandler(err) { $el.html('Error'); options.failure(err); } $.ajax(options.url, { success: successHandler, error: errorHandler });});
With Promises$.fn.loadAndShowContent(function(options) { var $el = this, fetchingContent = $.ajax(options.url);
fetchingContent.done(function(content) { $el.html(content); });
fetchingContent.fail(function(content) { $el.html('Error'); });
return fetchingContent;});
Merging Promises
var fetchingData = $.get('myData');var fadingButton = $button.fadeOut().promise();
$.when(fetchingData, fadingButton) .then(function() { // Both Promises have resolved});
Piping Promises
var fetchingPassword = $.get('/password');fetchingPassword.done(function(password) { var loggingIn = $.post('/login', password);});
// I wish I could attach listeners to the loggingIn// Promise here... but it doesn’t exist yet!
Piping Promisesvar fetchingPassword = $.get('/password');var loggingIn = fetchingPassword.pipe(function(password) { return $.post('/login', password);});
loggingIn.then(function() { // We’ve logged in successfully}, function(err) { // Either the login failed, or the password fetch failed});
// NOTE: As of jQuery 1.8, then and pipe are synonymous.// Use `then` for piping if possible.
Piping Promisesvar menuFilePromise = mainWindow.menu('file');var openFilePromise = menuFilePromise.pipe(function(file) { return file.openMenu();});var menuOpenPromise = openFilePromise.pipe(function(menu) { return menu.item('open');});var itemClickPromise = menuOpenPromise.pipe(function(item) { return item.click()});var createDialogPromise = itemClickPromise.pipe(function() { return window.createDialog("Promises rock!");});
A Promise-y Rocketfunction launchRocketAt(target) { var rocketDeferred = $.Deferred(); _.extend(rocketDeferred, {x: 0, y: 0, step: 0});
function moveRocket() { // Physics calculations go here... rocketDeferred.notify(step / 10); if (rocketDeferred.step === 10) { rocketDeferred.resolve(); } else { rocketDeferred.step += 1; setTimeout(moveRocket, 50); } }
moveRocket(); return rocketDeferred;}
Promise Drawbacks
• No standard
• jQuery, Promises/A, Promises/B...
• For maximum benefit, you’ll need wrappers all over the place
Pattern III: AMD
What is AMD?
• Asynchronous Module Definition, a spec
• Each module says which modules it needs
• The module’s “factory” is called after all of those modules are loaded
What is AMD for?
• Loading dependencies as needed
• Dependency injection (for tests)
• Gating features
How to use AMD
define('myModule', ['jQuery', 'Backbone'],function($, Backbone) { var myModule = { // Define some things... };
// If anyone requires this module, they get this object return myModule;});
AMD Drawbacks
• No standard
• Lots of up-front work
• No semantic versioning
• Heavyweight tools (RequireJS)
Alternatives to AMD
• Browserify
• Simple syntax: require('./filename');
• Great if you’re into Node + npm
• Intended for bundling, not so much for async module loading
Conclusion
• The next time you’re about to define a function with a callback argument... don’t.
Thanks. Questions?@trevorburnham