Sane Async Patterns

5 min read Original article ↗
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

    In This Talk •Callback arguments considered harmful • Three alternative patterns: • PubSub • Promises • AMD

  • 7.
  • 8.

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

  • 9.

    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) { ... }

  • 10.

    A Silver Lining myFunction1(); //No state changes here! myFunction2(); // Which means we never have to do this... while (!document.ready) { Thread.sleep(0); }

  • 11.
  • 12.

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

  • 13.

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

  • 14.
  • 15.
  • 16.

    What is PubSub? button.on("click",function(event) { ... }); server.on("request", function(req, res, next) { ... }); model.on("change", function() { ... });

  • 17.

    What is PubSubfor? • Just about everything! • When in doubt, use PubSub

  • 18.

    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

  • 19.

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

  • 20.

    An Evented Rocket varrocket = new Rocket(); rocket.launchAt(target).on('complete', function() { // Now it’s obvious what this callback is! });

  • 21.

    PubSub Drawbacks • Nostandard • Consider using LucidJS: https://github.com/RobertWHurst/ LucidJS

  • 22.
  • 23.

    What is aPromise? • “A promise represents the eventual value returned from the single completion of an operation.” —The Promises/A Spec

  • 24.

    What is aPromise? • An object that emits an event when an async task completes (or fails) Resolved Pending Rejected

  • 25.

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

  • 26.

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

  • 27.

    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/

  • 28.

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

  • 29.

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

  • 30.

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

  • 31.

    Merging Promises var fetchingData= $.get('myData'); var fadingButton = $button.fadeOut().promise(); $.when(fetchingData, fadingButton) .then(function() { // Both Promises have resolved });

  • 32.

    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!

  • 33.

    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.

  • 34.

    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!"); });

  • 35.

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

  • 36.

    Promise Drawbacks • Nostandard • jQuery, Promises/A, Promises/B... • For maximum benefit, you’ll need wrappers all over the place

  • 37.
  • 38.

    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

  • 39.

    What is AMDfor? • Loading dependencies as needed • Dependency injection (for tests) • Gating features

  • 40.

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

  • 41.

    AMD Drawbacks • Nostandard • Lots of up-front work • No semantic versioning • Heavyweight tools (RequireJS)

  • 42.

    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

  • 43.

    Conclusion • The nexttime you’re about to define a function with a callback argument... don’t.

  • 44.