$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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  6. Javascript's
    programming model

    View Slide

  7. View Slide

  8. View Slide

  9. View Slide

  10. View Slide

  11. Con$nua$on passing style

    View Slide

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

    View Slide

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

    View Slide

  14. Born on the server

    View Slide

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

    View Slide

  16. con$nua$on.js

    View Slide

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

    View Slide

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

    View Slide

  19. streamline.js

    View Slide

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

    View Slide

  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

    View Slide

  22. Can we do something with just a
    Library?

    View Slide

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

    View Slide

  24. async.js

    View Slide

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

    View Slide

  26. Library-only
    async.js
    • simple
    • minimal dependency
    • no build step
    • boilerplaty

    View Slide

  27. You promised me a
    Promise

    View Slide

  28. View Slide

  29. View Slide

  30. View Slide

  31. View Slide

  32. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  40. How a Promise can get you out of callback hell
    and into Promise hell

    View Slide

  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

    View Slide

  42. Enter the
    co-rou&ne

    View Slide

  43. View Slide

  44. async
    and
    await
    from the future
    (ES7)

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  50. Async / await
    ES7, babel + run.me
    • need build step (transpiler)
    • need run1me
    • clean, intui1ve syntax
    • requires promise-based api (promisify)

    View Slide

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

    View Slide

  52. And what about
    generators?
    You know, those with the * and
    yield

    View Slide

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

    View Slide

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

    View Slide

  55. Try them out,
    choose what works best
    in your environment

    View Slide

  56. View Slide

  57. Thanks!

    View Slide