$30 off During Our Annual Pro Sale. View Details »

milanojs - How a Promise can get you out of callback hell

milanojs - How a Promise can get you out of callback hell

In javascript si scrive spesso codice per reagire ad un evento o un’azione come un click su un pulsante nel browser o il ricevimento di una richiesta sul server: questo codice viene spesso fornito al sistema sotto forma di funzioni di callback, che verranno invocate al momento opportuno. Nella gestione di una serie di azioni ed eventi, questo stile di programmazione ("CPS") può risultare in codice "ad albero di natale”, costituito da tante funzioni anonime una dentro l’altra e tipicamente difficile da leggere, rifattorizzare e mantenere.

Esploreremo una serie di astrazioni e librerie volte a mitigare questo problema. Parleremo di Promise, fibers, estensioni alla sintassi come async/await e altro.

Andrea Lattuada

November 03, 2015
Tweet

More Decks by Andrea Lattuada

Other Decks in Technology

Transcript

  1. How a Promise can get you out of callback hell

    Andrea La)uada twi$er.com/utaal, github.com/utaal buildo
  2. function postMeetupComment(userId, meetupId, entry, callback) { db.getMeetupById(meetupId, function(err, meetup) {

    if (err) callback(err); if (!meetup.canceled) { db.getAttendees(meetupId, function(err, attendees) { if (err) callback(err); if (attendees.contains(userId)) { swearWordService.check(entry, function(err, clean) { if (err) callback(err); if (clean) { db.postComment(userId, meetupId, entry, callback); } else { callback("The post contains inappropriate words"); } }); } else { callback("User is not attending the meetup"); } } } else { callback("The meetup was canceled"); } }); }
  3. function postMeetupComment(userId, meetupId, entry, _) { var meetup = db.getMeetupById(meetupId,

    _); if (meetup.canceled) { throw "The meetup was canceled."; } var attendees = db.getAttendees(meetupId, _); if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } var clean = swearWordService.check(entry, _); if (!clean) { throw "The post contains inappropriate words"; } cb.postComment(userId, meetupId, entry, _); }
  4. $(document).ready(function () { $.get('data/meetup/milanojs.json', function(milanojs) { var events = [];

    milanojs.events.map(function (eId) { $.get('data/meetup/events/' + eId + '.json', function(evnt) { events.push(evnt); if (events.length == milanojs.events.length) { // we got all resopnses Object.assign(milanojs, { events: events }); $('.output').html(template(milanojs)); } }); }); }); });
  5. await promisify($(document).ready)(); var milanojs = await promisify($.get)('data/meetup/milanojs.json'); var events =

    milanojs.events.map(function (eId) { return await promisify($.get)('data/meetup/events/' + eId + '.json'); }); milanojs = Object.assign(milanojs, { events: events }); $('.output').html(template(milanojs));
  6. Javascript's programming model

  7. None
  8. None
  9. None
  10. None
  11. Con$nua$on passing style

  12. $(document).ready(function () { $.get('data/meetup/milanojs.json', function(milanojs) { var latestEventId = milanojs.events[0];

    $.get('data/metup/events/' + latestEventId + '.json', cont(evnt) { $('.output').html(template(evnt)); }); }); });
  13. function postMeetupComment(userId, meetupId, entry, callback) { db.getMeetupById(meetupId, function(err, meetup) {

    if (err) callback(err); if (!meetup.canceled) { db.getAttendees(meetupId, function(err, attendees) { if (err) callback(err); if (attendees.contains(userId)) { swearWordService.check(entry, function(err, clean) { if (err) callback(err); if (clean) { db.postComment(userId, meetupId, entry, callback); } else { callback("The post contains inappropriate words"); } }); } else { callback("User is not attending the meetup"); } } } else { callback("The meetup was canceled"); } }); }
  14. Born on the server

  15. Con$nua$on-passing-style transforma)on

  16. con$nua$on.js

  17. $(document).ready(function () { $.get('data/meetup/milanojs.json', function(milanojs) { var latestEventId = milanojs.events[0];

    $.get('data/metup/events/' + latestEventId + '.json', function(evnt) { $('.output').html(template(evnt)); }); }); }); ⁵ $(document).ready(cont()); $.get('data/meetup/milanojs.json', cont(milanojs)); var latestEventId = milanojs.events[0]; $.get('data/meetup/events/' + eId + '.json', cont(evnt)); $('.output').html(template(evnt));
  18. function postMeetupComment(userId, meetupId, entry, callback) { db.getMeetupById(meetupId, function(err, meetup) {

    if (err) callback(err); if (meetup.canceled) { callback("The meetup was canceled"); } else { db.getAttendees(meetupId, function(err, attendees) { if (err) callback(err); if (!attendees.contains(userId)) { callback("User is not attending the meetup"); } else { swearWordService.check(entry, function(err, clean) { if (err) callback(err); if (!clean) { callback("The post contains inappropriate words"); } else { db.postComment(userId, meetupId, entry, callback); } }); } } } }); }
  19. streamline.js

  20. function postMeetupComment(userId, meetupId, entry, _) { var meetup = db.getMeetupById(meetupId,

    _); if (meetup.canceled) { throw "The meetup was canceled."; } var attendees = db.getAttendees(meetupId, _); if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } var clean = swearWordService.check(entry, _); if (!clean) { throw "The post contains inappropriate words"; } cb.postComment(userId, meetupId, entry, _); }
  21. Con$nua$on-passing-style transform con$nua$on.js, streamline.js • requires transpiling (build step) •

    requires run3me (streamline.js) • intui3ve syntax • non-standard
  22. Can we do something with just a Library?

  23. function postMeetupComment(userId, meetupId, entry, callback) { db.getMeetupById(meetupId, function(err, meetup) {

    if (err) callback(err); if (!meetup.canceled) { db.getAttendees(meetupId, function(err, attendees) { if (err) callback(err); if (attendees.contains(userId)) { swearWordService.check(entry, function(err, clean) { if (err) callback(err); if (clean) { db.postComment(userId, meetupId, entry, callback); } else { callback("The post contains inappropriate words"); } }); } else { callback("User is not attending the meetup"); } } } else { callback("The meetup was canceled"); } }); }
  24. async.js

  25. function postMeetupComment(userId, meetupId, entry, callback) { async.waterfall([ function(callback) { db.getMeetupById(meetupId,

    callback); }, function(meetup, callback) { if (meetup.canceled) { callback("The meetup was candeled."); } else { db.getAttendees(meetupId, callback); } }, function(attendees, callback) { if (!attendees.contains(userId)) { callback("User is not attending the meetup"); } else { swearWordService.check(entry, callback); } }, function(clean, callback) { if (!clean) { callback("The post contains inappropriate words"); } else { db.postComment(userId, meetupId, entry, callback); } } ], callback); }
  26. Library-only async.js • simple • minimal dependency • no build

    step • boilerplaty
  27. You promised me a Promise

  28. None
  29. None
  30. None
  31. None
  32. None
  33. function timeoutPromise(delay) { return new Promise(function(resolve, reject) { setTimeout(function() {

    resolve(true) }, delay); }); } var p = timeoutPromise(20000); p.then(function() { console.log('hi!'); // printed 20 seconds after timeoutPromise was invoked });
  34. $(document).ready(function () { $.get('data/meetup/milanojs.json', function(milanojs) { var latestEventId = milanojs.events[0];

    $.get('data/metup/events/' + latestEventId + '.json', function(evnt) { $('.output').html(template(evnt)); }); }); }); → promisify($(document).ready)().then(function() { return promisify($.get)('data/meetup/milanojs.json'); }).then(function(milanojs) { var latestEventId = milanojs.events[0]; return promisify($.get)('data/meetup/events/' + eId + '.json'); }).then(function(evnt) { $('.output').html(template(evnt)); });
  35. $(document).ready(function () { $.get('data/meetup/milanojs.json', function(milanojs) { var events = [];

    milanojs.events.map(function (eId) { $.get('data/meetup/events/' + eId + '.json', function(evnt) { events.push(evnt); if (events.length == milanojs.events.length) { // we got all resopnses milanojs = Object.assign(milanojs, { events: events }); $('.output').html(template(milanojs)); } }); }); }); }); → promisify($(document).ready)().then(function() { return promisify($.get)('data/meetup/milanojs.json'); }).then(function(milanojs) { return Promise.all(milanojs.events.map(function (eId) { return promisify($.get)('data/meetup/events/' + eId + '.json'); })).then(function(events) { return Object.assign(milanojs, { events: events }); }); }).then(function(data) { $('.output').html(template(data)); });
  36. $.getPromise = function(url) { return new Promise(function(resolve, reject) { $.get(url,

    function(result) { resolve(result); }); }); } → $.getPromise = function(url) { return new Promise(function(resolve, reject) { $.get(url, resolve); }); }
  37. function postMeetupComment(userId, meetupId, entry, callback) { db.getMeetupById(meetupId, function(err, meetup) {

    if (err) callback(err); if (!meetup.canceled) { db.getAttendees(meetupId, function(err, attendees) { if (err) callback(err); if (attendees.contains(userId)) { swearWordService.check(entry, function(err, clean) { if (err) callback(err); if (clean) { db.postComment(userId, meetupId, entry, callback); } else { callback("The post contains inappropriate words"); } }); } else { callback("User is not attending the meetup"); } } } else { callback("The meetup was canceled"); } }); }
  38. var db = promisifyAll(db); function postMeetupComment(userId, meetupId, entry) { return

    db.getMeetupById(meetupId).then(function(meetup) { if (meetup.canceled) { throw "The meetup was candeled."; } else { return db.getAttendees(meetupId); } }).then(function(attendees) { if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } else { return swearWordService.check(entry); } }).then(function(clean) { if (!clean) { throw "The post contains inappropriate words"; } else { return db.postComment(userId, meetupId, entry); } }); }
  39. db = promisifyAll(db); function postMeetupComment(userId, meetupId, entry) { return db.getMeetupById(meetupId).then(function(meetup)

    { if (meetup.canceled) { throw "The meetup was candeled."; } else { return db.getAttendees(meetupId).then(function(attendees) { if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } else { return swearWordService.check(entry); } }).then(function(clean) { if (meetup.familySafe && !clean) { throw "The post contains inappropriate words"; } else { return db.postComment(userId, meetupId, entry); } }); } }); }
  40. How a Promise can get you out of callback hell

    and into Promise hell
  41. Promise ES6, polyfill • needs polyfill for IE and older

    browsers • no build step (no transforma8on) • standard (hopefully not going away) • syntax is some8mes cumbersome • need to wrap callback-based apis
  42. Enter the co-rou&ne

  43. None
  44. async and await from the future (ES7)

  45. $(document).ready(function () { $.get('data/meetup/milanojs.json', function(milanojs) { var events = [];

    milanojs.events.map(function (eId) { $.get('data/meetup/events/' + eId + '.json', function(evnt) { events.push(evnt); if (events.length == milanojs.events.length) { // we got all resopnses Object.assign(milanojs, { events: events }); $('.output').html(template(milanojs)); } }); }); }); }); → async function main() { await promisify($(document).ready)(); var milanojs = await promisify($.get)('data/meetup/milanojs.json'); var events = milanojs.events.map(function (eId) { return await $.get('data/meetup/events/' + eId + '.json'); }; milanojs = Object.assign(milanojs, { events: events }); $('.output').html(template(milanojs)); }); main();
  46. function main() { var milanojs, events, data; return _regeneratorRuntime.async(function main$(context$1$0)

    { while (1) switch (context$1$0.prev = context$1$0.next) { case 0: context$1$0.next = 2; return _regeneratorRuntime.awrap(promisify($(document).ready)); case 2: context$1$0.next = 4; return _regeneratorRuntime.awrap(promisify($.get)('data/meetup/milanojs.json')); case 4: milanojs = context$1$0.sent; console.log(milanojs); context$1$0.next = 8; return _regeneratorRuntime.awrap(_Promise.all(milanojs.events.map(function (ev) { return promisify($.get)('data/meetup/events/' + ev + '.json'); }))); case 8: events = context$1$0.sent; milanojs = _Object$assign(milanojs, { events: events }); console.log(data); $('.output').html(prettyHtml(milanojs)); case 12: case 'end': return context$1$0.stop(); } }, null, this); }
  47. function main() { var milanojs, events, data; return _regeneratorRuntime.async(function main$(context$1$0)

    { while (1) switch (context$1$0.prev = context$1$0.next) { case 0: context$1$0.next = 2; return _regeneratorRuntime.awrap(promisify($(document).ready)); case 2: context$1$0.next = 4; return _regeneratorRuntime.awrap(promisify($.get)('data/meetup/milanojs.json')); case 4: milanojs = context$1$0.sent; console.log(milanojs); context$1$0.next = 8; return _regeneratorRuntime.awrap(_Promise.all(milanojs.events.map(function (ev) { return promisify($.get)('data/meetup/events/' + ev + '.json'); }))); case 8: events = context$1$0.sent; milanojs = _Object$assign(milanojs, { events: events }); console.log(data); $('.output').html(prettyHtml(milanojs)); case 12: case 'end': return context$1$0.stop(); } }, null, this); }
  48. db = promisifyAll(db); function postMeetupComment(userId, meetupId, entry) { return db.getMeetupById(meetupId).then(function(meetup)

    { if (meetup.canceled) { throw "The meetup was candeled."; } else { return db.getAttendees(meetupId).then(function(attendees) { if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } else { return swearWordService.check(entry); } }).then(function(clean) { if (meetup.familySafe && !clean) { throw "The post contains inappropriate words"; } else { return db.postComment(userId, meetupId, entry); } }); } }); }
  49. db = promisifyAll(db); async function postMeetupComment(userId, meetupId, entry) { var

    meetup = await db.getMeetupById(meetupId)); if (meetup.canceled) { throw "The meetup was candeled."; } var attendees = await db.getAttendees(meetupId); if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } var clean = await swearWordService.check(entry); if (meetup.familySafe && !clean) { throw "The post contains inappropriate words"; } await db.postComment(userId, meetupId, entry); }
  50. Async / await ES7, babel + run.me • need build

    step (transpiler) • need run1me • clean, intui1ve syntax • requires promise-based api (promisify)
  51. db = promisifyAll(db); async function postMeetupComment(userId, meetupId, entry) { var

    meetup = await db.getMeetupById(meetupId)); if (meetup.canceled) { throw "The meetup was candeled."; } var attendees = await db.getAttendees(meetupId); if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } var clean = await swearWordService.check(entry); if (meetup.familySafe && !clean) { throw "The post contains inappropriate words"; } await db.postComment(userId, meetupId, entry); }
  52. And what about generators? You know, those with the *

    and yield
  53. Use source maps for debuggability, ES7 cannot come too soon

  54. db = promisifyAll(db); function postMeetupComment(userId, meetupId, entry) { Fiber(function() {

    var meetup = db.getMeetupById(meetupId); if (meetup.canceled) { throw "The meetup was candeled."; } var attendees = db.getAttendees(meetupId); if (!attendees.contains(userId)) { throw "User is not attending the meetup"; } var clean = swearWordService.check(entry); if (meetup.familySafe && !clean) { throw "The post contains inappropriate words"; } db.postComment(userId, meetupId, entry); }).run(); }
  55. Try them out, choose what works best in your environment

  56. None
  57. Thanks!