- Agenda
- Async Patterns (i.e. Callbacks)
- Generators / Coroutines
- Promises
setTimeout(function(){
console.log("callback!");
}, 1000);
-
We think of this in a synchronous manner. "I'm going to do something, wait 1000ms, and then do something else"
-
Our brains don't work asynchronously, we just switch contexts quickly
-
Callback Hell
setTimeout(function() {
console.log("one");
setTimeout(function(){
console.log("two");
setTimeout(function() {
console.log('three');
},1000);
},1000);
}, 1000);
- It's not callback-hell because of the indentation...
function one(cb) {
console.log("one");
setTimeout(cb, 1000);
}
function two(cb) {
console.log("two");
setTimeout(cb, 1000);
}
function three() {
console.log("three");
}
one(function() {
two(three);
});
- The above is still callback hell. Why?
- Inversion of Control
- As soon as we take our program, wrap it in a function, and hand it off to another function, we lose control.
- There are certain expectations we have when we work with callbacks, and it can add confusion to the code.
- Let's imagine you add a callback to a third-party API
- You pass a callback to a library
- The library calls your callback more times you expect it to, and it wasn't documented
- Things can get hidden / confusing
function trySomething(ok, err) {
setTimeout(function() {
var num = Math.random();
if (num > 0.5) ok(num);
else err(num);
}, 1000);
}
trySomething(
function(num) {
console.log("Success: " + num);
},
function(num) {
console.log("Failure: " + num);
},
);
-
The above doesn't solve IoC -- it makes it worse. What if both callbacks were called?
-
What about this "error=first" aka "node" style?
function trySomething(cb) {
setTimeout(function() {
var num = Math.random();
if (num > 0.5) cb(null, num);
else cb("Too low!");
}, 1000);
}
trySomething(function(err, num) {
if (err) {
console.log(err);
} else {
console.log("Number: " + num);
}
});
- What if the Error and the success value are passed back? You'd probably ignore the success value.
- The above does doesn't solve anything.
function getData(d, cb) {
setTimeout(function() { cb(d); }, 1000);
}
getData(10,function(num1)[
var x = 1 + num1;
getData(30, function(num2){
var y = 1 + num2;
getData(
"Meaning of life: " + (x + y)),
function(answer) {
console.log(answer); // Meaning of life: 42
}
);
});
});
Still a mess!!!
- Asynchronous code in a synchronous manner...
- This breaks an assumption of our JS code
- As soon as a function starts executing, we assume the function will finish all lines of its code before other code runs.
- That's true of normal functions, but not true of generators
- Generators pause themselves in the middle and can be resumed later.
function* gen() {
console.log("Hello");
yield null;
console.log("World");
}
var it = gen(); // this doesn't actually run our function, it just creates an iterator
it.next(); // prints "Hello"
it.next(); // prints World"
- On line 7,
var it = gen();
we haven't executed anything yet, just made an iterator. - The iterator controls operation of our function
- On line 8,
it.next()
, we start the generator and it runs until the first yield statement - We can call
it.next()
again and it will resume from where it paused and run to the nextyield
statement
var run = coroutine(function* () {
var x = 1 + (yeild null);
var y = 1 + (yield null);
yield (x + y);
});
run();
run10);
console.log("Meaning of life: " + run(30).value);
-
Looking at the above,
yield
is a two-way message-passing method -
on line 7, when we
run()
first,yield
gives us anull
value. -
So that
null
value is the return value of thatrun()
call -
when I say
run(10)
next, theyield
expression we stopped on gets replaced with 10 -
this results in
var x = 1 + (10)
getting executed, so x = 11 -
Then it goes to
var y = 1 + (yield null)
-- thus, the return value ofrun(10)
isnull
-
Then we can say
run(30)
, resutling invar y = 1 + 30
and so y = 31 -
the code then continues to execute upto the next yield expression, stopping on
yield (x + y)
and returning 31+11 = 42 -
the return value of
run(30)
becomes 42 -
But how does this help with async?
-
Well, our steps do not synchronously iterate.
function getData(d) {
setTimeout(function() { run(d); }, 1000);
}
var run = coroutine(function*() {
var x = 1 + (yield getData(10));
var y = 1 + (yield getData(30));
var answer = 1 + (yield getData("Meaning of life: " + (x + y)));
console.log(answer);
});
run();
- The above might look synchronous, but it's actually async
- Lots of ways people explain promises
- Here's two metaphores:
- Order number / receipt
- You give casheir some money for some food
- Casheir hands you a receipt with an order number
- Casheir calls out your number, you give them the receipt and they give you the food
- Promises will return a "receipt" which you can "exchange" for the value you asked for when it's ready
- Continuation events
- What if we could subscribe to an event that lets us know when that function finishes?
- That's essentially what a promise is
- We call the function, it doesn't finish
- we subscribe to a "continuation event" which fires when the function finishes
This is how jQuery does it... (not standard)
var wait = jQuery.Deferred(); // a 'deferred' object is the receipt
var p = wait.promise(); // this is getting our continuation event
p.done(function(value) { // here we subscribe to the continuation event
console.log(value);
});
setTimeout(function() {
wait.resolve(Math.random()); // this is where we say "order is ready", or where we trigger the continuation event
}, 1000);
- Below is an example of how we'd use the above...
function waitForN(n) {
var d = $.Deferred();
setTimeout(d.resolve, n);
return d.promise();
}
waitForN(1000)
.then(function(){
console.log("Hello world");
return waitForN(2000);
})
.then(function() {
console.log("finally!");
});
- The above is very synchronous-looking syntax for an asynchronous series of tasks.
- When I pass a callback into a utility, the utility has control over when my callback is invoked.
- With a promise, the utility gives us a promise back.
- We listen to the promise and decide what to do next. We are now in control.
Promises are built into ES6 and they're already there in Chrome.
function getData(d) {
return new Promise(function(resolve, reject) {
setTimeout(function() { resolve(d); }, 1000);
});
}
var x;
getData(10)
.then(function(num1) {
x = 1 + num1;
return getData(30);
})
.then(function(num2) {
var y = 1 + num2;
return getData("Meaning of life: " + (x + y));
})
.then(function(answer){
console.log(answer);
});
- A library written by Kyle
- useful for more complex tasks -- a Promise-abstraction library.
- examples of Promise-abstraction libraries: async, queue, asyncquence
- Squence = automatically chained promises
ASQ()
.then(function(done)[
setTimeout(done, 1000);
})
.gate( // two or more things happen at the same time, wait for everyone to finish in whatevre order
function(done){
setTimeout(done, 1000);
},
function(){
setTimeout(done, 1000);
}
)
.then(function(){
console.log("2 seconds passed!");
});
another example
function getData(d) {
return ASQ(function(done){
setTimeout(function(){ done(d); }, 1000);
});
}
ASQ()
.waterfall(
function(done){ getData(10).pipe(done); },
function(done({ getData(30).pipe(done); }
)
.seq(function(num1, num2) {
var x = 1 + num1;
var y = 1 + num2;
return getData("Meaning of life: " + (x + y));
})
.val(function(answer) {
console.log(answer);
// Meaning of life: 42
});
What about Generators + Promises? Well this is also built into ASQ (runner
)
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)
));
return answer
})
.val(function(answer){
console.log(asnwer); // meaning of life...
});
Here's something else cool...
ASQ(2)
.runner(
// v1 will be set as `2` here
function*(v1) {
console.log("initial v1:" + v1); // initial v1: 2
v1 += yield null;
console.log("now v1:" + v1); // now v1: 12
v1 += yield getData(v1 * 2);
console.log("finally v1:" + v1); // finally v1: 84
},
function*() {
var v2 = yield getData(10);
console.log("initial v2: " + v2); // initial v2: 24
v2 += yield getData(v2 *3);
console.log("finally v2: " + v2); // finally v2: 109
yielf getData(v2 + 6);
}
)
.val(function(msg) {
console.log("Result: " + msg); // result 115
});
- What is "callback hell"? Why do callbacks suffer from "inversion of control"?
- Giving up control of your program to another function
- How do you pause a generator? How do you resume it?
yield
andnext
respectively
- What is a promise? How does it solve inversion of control issues?
- It's a "promise" for a future value, a method of continuing control.
- Rather than passing a method in, we get the value out
- It inverts the inversion of control
- How do we combine generators and promises for flow control?
- The generator yields out promises
- When the promise finishes, it restarts the generator
- ASQ's method is called
runner
- Load three seperate files in parallel
- print them in the proper order
- It's currently implemented as callbacks
- Use promises or genrators or whatever to reimplement it.