In This Talk •Callback arguments considered harmful • Three alternative patterns: • PubSub • Promises • AMD
Pyramid of Doom mainWindow.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 slide try { 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); }
Nested Spaghetti mainWindow.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 APIs function 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(); }
What is PubSub? button.on("click",function(event) { ... }); server.on("request", function(req, res, next) { ... }); model.on("change", function() { ... });
What is PubSubfor? • Just about everything! • When in doubt, use PubSub
How to useit? • Pick a PubSub library, such as https://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 Rocket Rocket.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 varrocket = new Rocket(); rocket.launchAt(target).on('complete', function() { // Now it’s obvious what this callback is! });
PubSub Drawbacks • Nostandard • Consider using LucidJS: https://github.com/RobertWHurst/ LucidJS
What is aPromise? • “A promise represents the eventual value returned from the single completion of an operation.” —The Promises/A Spec
What is aPromise? • An object that emits an event when an async task completes (or fails) Resolved Pending Rejected
Example 1: Ajax varfetchingData = $.get('myData'); fetchingData.done(onSuccess); fetchingData.fail(onFailure); fetchingData.state(); // 'pending' // Additional listeners can be added at any time fetchingData.done(celebrate); // `then` is syntactic sugar for done + fail fetchingData.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 aPromise? • “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 // APromise is a read-only copy of a Deferred var 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 Promises var 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 Promises var 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 Rocket functionlaunchRocketAt(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 • Nostandard • jQuery, Promises/A, Promises/B... • For maximum benefit, you’ll need wrappers all over the place
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 AMDfor? • Loading dependencies as needed • Dependency injection (for tests) • Gating features
How to useAMD define('myModule', ['jQuery', 'Backbone'], function($, Backbone) { var myModule = { // Define some things... }; // If anyone requires this module, they get this object return myModule; });
AMD Drawbacks • Nostandard • 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 nexttime you’re about to define a function with a callback argument... don’t.