Asynchronous JavaScript
Table of Contents
Parallel vs Async
- Parallelism is about efficiency and this is often achieved via threads.
- Multiple tasks running at the same time.
- Asynchronicity is often required when working with a single thread (JS is single threaded).
- Concurrency: 2 higher level tasks happening within the same time frame.
- Asynchronous programming revolves around managing concurrency.
Callback
- Callbacks == Continuations
- 2 Callback Hell Problems:
- Inversion of Control: A part that you are in control of executing and another part that you are not in control of.
This refers to code you give to either another function or something that determines how that code is run. You are not in control.
- When we call a callback we should be checking for the following things to ensure trust in the callback:
- not too early
- not too late
- not too many times
- not too few times
- no lost context
- no swallowed errors
- When we call a callback we should be checking for the following things to ensure trust in the callback:
- They're not reasonable: they are not able to be reasoned about.
- Inversion of Control: A part that you are in control of executing and another part that you are not in control of.
This refers to code you give to either another function or something that determines how that code is run. You are not in control.
- Temporal Dependency refers to one function relying on the completion of another function (callbacks).
Thunks
-
Synchronously is a function that already has everything it needs to give a value back (no arguments required)
var thunk = function() { return add(10,15); }
-
Asynchronously is a function that doesn't need any args to do its job, it only needs a callback to get a value out.
function addAsync(x,y,cb) { setTimeout(function() => { cb( x+y ) }, 1000); }; var thunk = function(cb) { addAsync(10,15, cb); }; thunk(function(sum){ sum; //25 });
-
One of the best parts about this tool (preserving state) we can eliminate worrying about time, time is no longer an issue.
-
It is a certain type of coding pattern that can abstract issues such as order of completion... time.
-
Good example from Uncle Kyle:
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); } // ************************************** function getFile(file) { var resp; fakeAjax(file,function(text){ if (!resp) resp = text; else resp(text); }); return function th(cb) { if (resp) cb(resp); else resp = cb; }; } // request all files at once in "parallel" var th1 = getFile("file1"); var th2 = getFile("file2"); var th3 = getFile("file3"); th1(function ready(text){ output(text); th2(function ready(text){ output(text); th3(function ready(text){ output(text); output("Complete!"); }); }); });
Promises
- Solves the Inversion of Control paradigm, gives us control.
- Analogy: Buying a cheeseburger, you don't get the cheeseburger right after the transaction. You are given a receipt as a promise or placeholder that you will get the cheeseburger later, when it's ready.
- A promise is either going to return successful or rejected, it can either fail or complete.
- Below is an example of how we can use a promise.
function trackCheckout(info) {
return new Promise(
function(resolve, reject){
// attempt to track the checkout
// if successful call resolve()
// otherwise, call reject(error)
}
)
}
function finish(){
chargeCreditCard(purchaseInfo);
showThankYouPage();
}
function error(err){
logStatsError(err);
finish();
}
var promise = trackCheckout(purchaseInfo);
promise.then(
finish,
error
);
- Promises are designed to instill trust in a transaction:
- only resolved once.
- either success OR error.
- messages passed/kept.
- exceptions become errors.
- immutable once resolved.
- Flow Control: We can manage our flow control with promises via chaining promises.
- Chaining promises happen by putting the return of the second promise into the "then" of the first promise.
doFirstThing().then(function(){ return doSecondThing(); }).then(function(){ return doThirdThing(); }).then( complete, error );
- Abstractions:
Promise.all
: takes an array of promises and waits for all of them to finish then produces an array of results. Same order they were called in, not the order in which they finished.- requires all promises in the array to complete with no errors.
Promise.race
: Takes an array of promises and waits for the first resolution or failure and ignores the rest.Promise.any
: Takes an array of promises and waits for the first fulfilled promise, if all reject it will throw an error.Promise.alLSettled
: similar to Promise.all but will reject as a whole if any promise in the array rejects.
- Sequences and Gates:
- Using a library, asynquence / ASQ:
- you can use a
.gate
instead of.then
which acts like a Promise.all, you can pass it different steps and it will wait for all to complete. .waterfall
allows you to have multiple steps and each step passes its result into the next step..seq
: used fir sequencing the async steps, calling the promise aware functions..val
: used for synchronous steps, like outputting a result for example.
- you can use a
- Using a library, asynquence / ASQ:
- Creating a promise aware utility is referred to as lifting.
- Catch errors that propagate through the chain with a
.catch
or you can catch errors by catching it there.promise1 .then( output, function(err) { // deal with error } ) .then(output2) .then(output3) .catch(function(err){ //deal with error at the end. })
- This code demonstrates catching an error at the end or supplying an error function into the callback to deal with it there.
- Avoid nesting, its poor styling and defeats the point by missing the beauty of promise chains. We don't want to go to PROMISE HELL.
Generators
- Syntactic form of declaring a state machine.
- Generators alone don't solve inversion of control, they do solve the reasonability issue.
- Special keyword yield, acts like a pause button.
- Think of a generator as a pauseable and resumable function.
- while paused, everything inside the generator is blocked.
- Fantastic for writing synchronous looking asynchronous code.
- Essentially writing synchronous blocked code and hiding the asynchronicity.
- The generator itself is synchronous, so we can essentially do asynchronous tasks and bring those things into our synchronous stack.
function* gen() { // * tells JS that this function is a generator
console.log("Hello");
yield;
console.log("World");
}
var it = gen();
it.next();
it.next();
- Calling a generator, doesn't start the generator, it produces an iterator to which we can step through.
- Generators have properties:
value
anddone
.
function *main() {
yield 1;
yield 2;
yield 3;
}
var it = main();
it.next(); // { value: 1, done: false }
it.next(); // ( value: 2, done: false )
it.next(); // { value: 3, done: false }
it.next(); // { value: undefined, done: true }
- You can use
yield
in a generator as a placeholder, simply surround in parentheses and when we get to yield the generator will pause and wait for a value.
function* gen() {
var x = 1 + (yield); // 2. Waits here for a value to passed in
var y = 1 + (yield); // 3. Will wait here for a value to be passed in
yield (x + y); // 5. generator will yield the final value which can be accessed with .value
}
var it = gen();
it.next(); // 1. Will go to the first yield and wait
it.next(10); // 3. Continues the generator and passes in the value 10 to where we were paused
console.log(
"Meaning of life: " + it.next(30).value // 4. Will pass the value into the the placeholder.
);
Generators & Promises
-
Solves the 2 big problems!
- Inversion of Control
- They're not reasonable
-
yield promise, this is how we work promises into the generator.
- yields out a promise and then starts the generator.
- ASQ has this built in, an example of using this is below:
function getData(d) { return ASQ(function(done){ setTimeout(function(){ done(d); }, 1000); }); } ASQ() .runner(function*() { var x = 1 + (yield getData(10)); var y = 1 + (yield getData(30)); var answer = yield (getData( "Meaning of life: " + (x + y) )); yield answer; }) .val(function(answer){ console.log(answer); // Meaning of life: 42 });
-
Partnering this with ASQ we can get the below extremely clean code implementation:
ASQ.runner(function *main() {
var p1 = getFile("file1");
var p2 = getFile("file2");
var p3 = getFile("file3");
output(yield p1);
output(yield p2);
output(yield p3);
output("Complete!");
})
Observables
- An observable is an adapter hooked on to an event source that produces a promise every time a new event comes through.
- Not natively in the language yet, most likely will end up in the language. At the moment only accessible through a library
- RxJS: library for observables
- A data flow mechanism designed to respond to events.
// fromEvent takes a DOM element and an event name
// Every time one of those events fire it pumps a piece of data through the chain.
var obsv = Rx.Observable.fromEvent(btn, "click");
// Declarative chain representing the data flow.
obsv
.map(function mapper(evt) {
return evt.target.className;
})
.filter(function filterer(className) {
return /foobar/.test(className);
})
// first time data comes through let it through, but if something that's already been through comes along immediately after, don't let it in
// 1 1 1 2 2 1 2 2 3 3 2 2
// distinctUntilChanged will results in letting the following bits of data through
// 1 2 1 2 3 2
.distinctUntilChanged()
// synchronous response to whatever is coming in, no transformation is happening
.subscribe(function(data){
var className = data[1];
console.log(className);
});
-
Reactive sequences are similar to Observables and are very good for pumping data into.
-
Example using a reactive sequences:
var rsq1 = ASQ.react.of(); var rsq2 = ASQ.react.of(1,2,3); var x = 10; setInterval(function(){ rsq1.push(x++); rsq2.push(x++); }, 500); rsq1.val(function(v){ console.log("1:". v); }); // 1:10 1:12 1:14 rsq2.val(function(v){ console.log("2:", v); }); // 2:1 2:2 2:3 2:11 2:13
-
The whole point of this is to model data flow with stream operations.
Communicating Sequential Processes
- Modelling concurrency with channels.
- A channel is like a pipe with no buffer size, meaning it has this notion of back pressure built in.
- Channels cant take info if its not ready to take it, likewise the channel wont send if its receiver isn't ready to receive.
- Modelling application with lots of tiny independent pieces (processes)
var ch = chan();
function *process1() {
yield put(ch, "Hello");
var msg = yield take(ch);
console.log(msg);
}
function *process2() {
var greeting = yield take(ch);
yield put(ch, greeting + " World");
console.log("done!");
}
// Hello World
// done!
-
put()
will put things into the channel. -
take()
will receive something from the channel. -
Can use this in conjunction with yields so that generators can wait until the channel gives it what it wants.
- This is what allows generators to link together whilst also being independent... this is where the power lies.
-
Below is an example using a library and Block Channels:
csp.go(function*() { while(true) { yield csp.put(ch, Math.random()); } }); csp.go(function*() { while(true) { yield csp.take( csp.timeout(500) ); var num = yield csp.take(ch); console.log(num); } });
-
We can see we have 2 generators that are always going to be running.
-
The second one will only receive items from the channel every 0.5 seconds, this is to limit the amount it receives because the first generator would essentially spam it otherwise.
-
ASQ has the same syntax as
csp.go
however ASQ automatically creates a channel for you... Thanks Kyle.