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.

98e8177ac82d61a351b3b636e244bbd5?s=128

Andrea Lattuada

November 03, 2015
Tweet

Transcript

  1. 1.

    How a Promise can get you out of callback hell

    Andrea La)uada twi$er.com/utaal, github.com/utaal buildo
  2. 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. 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. 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. 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. 7.
  7. 8.
  8. 9.
  9. 10.
  10. 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)); }); }); });
  11. 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"); } }); }
  12. 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));
  13. 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); } }); } } } }); }
  14. 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, _); }
  15. 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
  16. 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"); } }); }
  17. 24.
  18. 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); }
  19. 28.
  20. 29.
  21. 30.
  22. 31.
  23. 32.
  24. 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 });
  25. 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)); });
  26. 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)); });
  27. 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); }); }
  28. 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"); } }); }
  29. 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); } }); }
  30. 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); } }); } }); }
  31. 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
  32. 43.
  33. 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();
  34. 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); }
  35. 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); }
  36. 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); } }); } }); }
  37. 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); }
  38. 50.

    Async / await ES7, babel + run.me • need build

    step (transpiler) • need run1me • clean, intui1ve syntax • requires promise-based api (promisify)
  39. 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); }
  40. 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(); }
  41. 56.
  42. 57.