These are some notes-to-self from Kyle Simpson‘s recenct ForwardJS workshop titled: Advanced JS: Rethinking Async.
The Problem: Callback Hell
- Limitations placed on our flow control, imposed by callback syntax.
- Inability to trust how your callback will be handled.
Concepts: Parallel vs Async
- Parallel = all 30 people on the same rollercoaster car.
- Async = 30 people on 30 1-seater rollercoaster cars one after the next.
Concurrency
- Temporal dependency – when a concurrent task depends on going before/after another concurrent task.
- Our brains are a good model of concurrency: “multi-tasking”
Inversion of Control
- You call libraries, frameworks call you.
- An async/callback model hands over (inverts) control to somewhere else in the system. This hands the control over, which can lead to complexity.
- Example: passing a callback to a 3rd party tracking function that calls your chargeCreditCard() method. If they push some experimental code that tries to retry the callback X number of times after a fixed timeout, the credit card gets charged multiple times. Not good.
Bugs
Bugs are more likely to happen when the way our brain works is different from how the code actually works behind the scenes.
The way we plan is the way we code, and this is typically sequential.
The way we express our code is fundamentally different from the way our brains think.
Thunks
NOTE TO SELF: this still does not make much sense to me – read more 🙂
In computer programming, a thunk is a subroutine that is created, often automatically, to assist a call to another subroutine
A function that has everything it needs (no args required, maybe a callback).
– Kyle
Synchronous example:var thunk = function() { return 10 + 20; };
- Old school method for handling race conditions and async.
- Thunk = time-independent wrapper around a future value.
Promises
- A way of promising a future value.
- A callback manager that addresses the “trust problems” associated with callback hell.
Analogy:
- Order a cheeseburger.
- Get a receipt (not the actual cheeseburger) = reference (promise) to future cheeseburger.
- At some future point, the number on receipt is called and the cheeseburger arrives.
Plusses:
- Promises are a way to preserve control of the callback (rather than “inverting” control).
- Promises natively handle all the issues associated with callback hell: risk of being called more than once, silent errors, etc.
Chaining Promises:
- This is how we get basic flow control.
- Method: return the
nth + 1
promise from the success handler of thenth
promise, which causes the subsequent.then()
to only resolve when the nested promise resolves.
Looks something like:
promise1 .then(function() { return promise2; }) .then(function() { return promise2; }) . . .
Promise Abstractions
Promise.all()
Takes an array of promises and then resolves them all with a callback that is passed a results
array, with indexes matching the original promises array.
Promise.race()
Pass an array of promises with one timer/placeholder promise that includes base timeout that returns a reject()
response within a settimeout()
. The function returns the first promise to resolve. This is a method for implementing a timeout for multiple promises.
Asynquence
https://github.com/getify/asynquence
- Promise-style async sequence flow-control made by Kyle
- This library is essentially a handy wrapper for promises that makes writing and reading promise-based code much clearer.
Generators
Example: function* gen() {...}
- Paused by
yield;
- Resumed by
iterator.next();
(whereiterator = gen()
)
* Note that yield
/next()
only pause the internal workings of the generator, not your entire JS environment.
- Generators allow us to implement the otherwise complicated go/pause patterns that our brains tend to like.
- One potential issue is that we still have a callback, which means we’re again inverting control, hence. . .
Generators + Promises
Takes the above pattern and simply returns a promise (we control) instead passing a callback. Confusing? Yes. But ends up with very simple code:
function fakeAjax(url,cb) { var fake_responses = { "file1": "The first text", "file2": "The middle text", "file3": "The last text" }; var randomDelay = (Math.round(Math.random() * 1E4) % 8000) + 1000; console.log("Requesting: " + url); setTimeout(function(){ cb(fake_responses[url]); },randomDelay); } function output(text) { console.log(text); } // This is where the magic happens! ↴ function getFile(file) { return ASQ(function(done){ fakeAjax(file,done); }); } var p1 = getFile("file1"); var p2 = getFile("file2"); var p3 = getFile("file3"); ASQ() .runner(function *main() { output( yield p1 ); output( yield p2 ); output( yield p3 ); output( 'So much better!' ); });
Looking back at what we might have without using Promises/Generators at all, it looks pretty awesome:
function fakeAjax(url,cb) { var fake_responses = { "file1": "The first text", "file2": "The middle text", "file3": "The last text" }; var randomDelay = (Math.round(Math.random() * 1E4) % 8000) + 1000; console.log("Requesting: " + url); setTimeout(function(){ cb(fake_responses[url]); },randomDelay); } function output(text) { console.log(text); } // ************************************** // The old-n-busted callback way function getFile(file) { fakeAjax(file,function(text){ maybePrintFiles(file,text); }); } function maybePrintFiles(file, text) { var files = ["file1","file2","file3"]; var rendered = []; if ( ! responses[file] ) { responses[file] = text; } for (var i=0; i<files.length; i++) { if ( true ) { console.log(rendered); console.log(rendered[files[i-1]]); if ( i==0 || null != rendered[files[i-1]] ) { console.log( text ); rendered[files[i]] = true; } } else { return; } console.log('Done!'); } } var responses = []; // request all files at once in "parallel" getFile("file1"); getFile("file2"); getFile("file3");
Observables
RxJS or Reactive Extensions for JavaScript is a library for transforming, composing, and querying streams of data. We mean all kinds of data too, from simple arrays of values, to series of events (unfortunate or otherwise), to complex flows of data.
Here’s a neat example using Kyle’s Asynquence library:
$(document).ready(function(){ var $btn = $("#btn"), $list = $("#list"), clicks = ASQ.react.of(), msgs = ASQ.react.of(), latest; $btn.click(function(evt){ // push click event messages into stream clicks.push(evt); }); // sample clicks stream setInterval(function(){ if (latest) { msgs.push("clicked!"); latest = null; } },1000); // subscribe to click stream clicks.val(function(evt){ latest = evt; }); // subscribe to sampled message stream msgs.val(function(msg){ $list.append($("<div>" + msg + "</div>")); }); });
Pingback: Notes from ForwardJS - General Sessions - Mickey Kay