315 lines
9.3 KiB
Markdown
315 lines
9.3 KiB
Markdown
+++
|
|
title = "async"
|
|
draft = false
|
|
tags = [
|
|
"code",
|
|
"javascript",
|
|
"async"
|
|
]
|
|
date = "2014-07-23"
|
|
+++
|
|
# Async coding in JS
|
|
|
|
[Asynchronous programming in JS: APIs interview](http://www.infoq.com/articles/surviving-asynchronous-programming-in-javascript) (infoQ)
|
|
|
|
## Het probleem
|
|
|
|
Alle events in javascript zijn asynchroon. Dat wil zeggen dat we geen idee hebben wanneer de eigenlijke code uitgevoerd is, en we een **callback closure** moeten meegeven, die verder werkt als de asynchrone code uitgevoerd is.
|
|
|
|
Dit is oké voor 1-2 asynchrone calls. Maar stel u voor dat we 4+ async calls moeten maken om werk gedaan te krijgen. Typisch dingen zoals:
|
|
|
|
* setTimeouts
|
|
* animaties (jQuery ea)
|
|
* AJAX calls (REST, naar domein logica, bewaren, opvragen, veranderen, ...)
|
|
|
|
### Een integratietest schrijven in JS
|
|
|
|
In *Java* kunnen we gewoon wat methods oproepen die data persisteert, daarna de eigenlijke *asserts* schrijven en eventueel in de `@After` met JUnit data cleanup uitvoeren:
|
|
|
|
```java
|
|
DomainObj obj = new DomainObjPersister()
|
|
.withA()
|
|
.withLala("lala")
|
|
.persist();
|
|
|
|
ChildObj child = new ChildObjPersister()
|
|
.withParent(obj)
|
|
.persist();
|
|
|
|
assertThat(child.getStuff()).isNotEmpty();
|
|
```
|
|
|
|
Om `child` te kunnen persisteren moeten we `obj` als parent meegeven, dus die call moet eerst uitgevoerd zijn. Alle persisters gaan naar de database. Dit zou in javascript zoiets zijn=
|
|
<img style='float: left; width: nolink&|px;' src='/img//code/javascript/kill-it-with-fire.gif'>
|
|
|
|
```javascript
|
|
$.ajax('/domain/obj/store', {
|
|
success: function(obj) {
|
|
$.ajax('/domain/child/store', {
|
|
success: function(child) {
|
|
assertThat(child.getStuff()).isNotEmpty();
|
|
}, ...
|
|
});
|
|
},
|
|
type: 'PUT',
|
|
dataType: 'json',
|
|
data: JSON.stringify({ key: 'value', key2: 'value2' })
|
|
});
|
|
```
|
|
|
|
Dus een callback wrappen in een callback wrappen in een callback.
|
|
|
|
#### Async event loop hulpjes
|
|
|
|
Zie ook [Philip Roberts: Help, I'm stuck in an event-loop](http://vimeo.com/96425312)
|
|
|
|
<img style='float: left; width: nolink |px;' src='/img//code/javascript/476470428_960.jpg'>
|
|
|
|
Tooltje om event loop te visualiseren zodat je ziet wat er gebeurt. Breaken in chrome helpt natuurlijk ook, gewoon naar de call stack kijken...
|
|
|
|
#### Asynchroon testen in Jasmine
|
|
|
|
Met **Jasmine** is het (beperkt) mogelijk om te wachten tot dat een stukje werk uitgevoerd is voordat de assertions afgegaan worden. <br/><br/>
|
|
Dit kan op de volgende manier:
|
|
|
|
```javascript
|
|
|
|
it("should be green, right??", function() {
|
|
var asyncCallFinished = false;
|
|
function callback(someObj) {
|
|
asyncCallFinished = true;
|
|
}
|
|
doAsyncCall(callback);
|
|
|
|
waitsFor(function() {
|
|
return asyncCallFinished ##### true;
|
|
});
|
|
|
|
runs(function() {
|
|
expect(stuff).toBeLotsBetter();
|
|
});
|
|
|
|
});
|
|
|
|
```
|
|
|
|
Pitfalls:
|
|
|
|
* Ge moet closure scope gebruiken om een variabele bij te houden om te controleren of de async call klaar is in een callback
|
|
* Ge moet `waitsFor()` gebruiken, intern pollt Jasmine waarschijnlijk gewoon...
|
|
* Ge moet eigenlijke assertions wrappen in `runs()` omdat `waitsFor()` direct retourneert en opzich async is.
|
|
|
|
De assertion functiepointers die meegegeven worden met `runs()` worden intern opgeslaan en bijgehouden totdat de closure van `waitsFor()` `true` retourneert. Daarna wordt ook alles pas meegegeven met de Jasmine reporter (logging, output etc). Redelijk omslachtig, aangezien 3+ async calls dan `waitsFor()` moeten wrappen. Geen oplossing.
|
|
|
|
##### Asynchroon testen met QUnit #####
|
|
|
|
```javascript
|
|
asyncTest("should be green, right??", function() {
|
|
var stuff = gogo();
|
|
function callback(obj) {
|
|
equal(obj.stuff, 2);
|
|
start();
|
|
}
|
|
|
|
doAsyncCall(callback);
|
|
|
|
});
|
|
```
|
|
|
|
Pitfalls:
|
|
|
|
* In de callback van uw async stuk code moeten zoals verwacht uw assertions zitten
|
|
* Ge moet een speciale test method gebruiken, `asyncTest()`
|
|
* Ge moet na uw assertions `start()` aanroepen (??)
|
|
|
|
#####== De oplossing #####==
|
|
|
|
https://github.com/willconant/flow-js e.a. (of iets zelfgemaakt in die aard).
|
|
|
|
Herneem bovenstaande integratietest code in javascript, maar dan met flow.js geschreven:
|
|
|
|
```javascript
|
|
flow.exec(
|
|
function() {
|
|
$.ajax('/domain/obj/store', {
|
|
success: this,
|
|
type: 'PUT',
|
|
dataType: 'json',
|
|
data: JSON.stringify({ key: 'value', key2: 'value2' })
|
|
});
|
|
},
|
|
function(obj) {
|
|
$.ajax('/domain/child/store', {
|
|
success: this, ...
|
|
});
|
|
},
|
|
function(child) {
|
|
assertThat(child.getStuff()).isNotEmpty();
|
|
}
|
|
);
|
|
```
|
|
|
|
Pitfalls:
|
|
|
|
* Error handling wordt opgefreten - gebruik Firebug's **debug on all errors** knop in de console. (anders mechanisme maken dat ze doorgooit van closure1 naar 2 ea)
|
|
* `curry()` gaat niet lukken aangezien de `this` pas in de closure zelf de juiste waarde krijgt.
|
|
* `this` moet meegegeven worden als callback, dus te intensief gebruik makend van `this` pointer in eigen code kan BOEM geven.
|
|
|
|
flow.js geeft het resultaat van closure1 mee als argument aan closure2 (via `arguments`) en zo maar door, dat is mega handig.
|
|
|
|
##### Synchrone code code combineren met asynchrone in flow.js #####
|
|
|
|
Enige minpunt is dat de callback `this()` moet expliciet aangeroepen worden om van closureX naar closureY over te stappen! <br/><br/>
|
|
Los dit op met een utility functie:
|
|
|
|
```javascript
|
|
flow.sync = function(work) {
|
|
return function() {
|
|
this(work.apply(this, Array.prototype.slice.call(arguments, 0)));
|
|
}
|
|
}
|
|
```
|
|
|
|
Zodat we dit kunnen doen:
|
|
|
|
```javascript
|
|
flow.exec(
|
|
function() {
|
|
asyncStuff(this);
|
|
},
|
|
flow.sync(function(resultfromPrev) {
|
|
console.log("lol"); // no this() required afterwards
|
|
}),
|
|
function(resultFromSyncStuff) {
|
|
doMoreAsyncStuff(this);
|
|
}
|
|
);
|
|
```
|
|
|
|
##### In een asynchrone closure parallel werken #####
|
|
|
|
Gebruik `this.MULTI()` als callback ipv `this` (zie voorbeeld hieronder)
|
|
|
|
##### flow.js combineren met Jasmine #####
|
|
|
|
Om de smeerlapperij van `waitsFor()` weg te werken kunnen we ook `flow.exec` gebruiken.
|
|
|
|
:exclamation: De laatste stap gaat **altijd** een `runs()` moeten bevatten voor de reporter om aan te duiden dat assertions uitgevoerd worden, aangezien de `exec()` functie direct retourneert. Dus moeten we 1x wachten, totdat de hele "flow" gedaan is. We kunnen dit combineren met BDD en een mini-DSL hierrond schrijven. Resultaat:
|
|
|
|
```javascript
|
|
function when() {
|
|
var flowDone = false;
|
|
var slice = Array.prototype.slice;
|
|
var argsArray = slice.call(arguments, 0);
|
|
var laatsteArgumenten;
|
|
|
|
argsArray.push(function() {
|
|
laatsteArgumenten = slice.call(arguments, 0);
|
|
flowDone = true;
|
|
});
|
|
|
|
flow.exec.apply(this, argsArray);
|
|
waitsFor(function() {
|
|
return flowDone ##### true;
|
|
});
|
|
|
|
return {
|
|
then: function(assertionsFn) {
|
|
runs(function() {
|
|
assertionsFn.apply(this, laatsteArgumenten);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
```
|
|
|
|
Voorbeeldcode:
|
|
|
|
```javascript
|
|
|
|
describe("plaatsen domein", function() {
|
|
it("wanneer ik alle plaatsen ophaal, kan ik hier domeinspecifieke functies aan opvragen", function() {
|
|
var plaatsen;
|
|
when(
|
|
function() {
|
|
DOMEIN.plaatsRepository.bewaarPlaats(plaats, this.MULTI());
|
|
DOMEIN.plaatsRepository.bewaarPlaats(anderePlaats, this.MULTI());
|
|
},
|
|
function() {
|
|
DOMEIN.plaatsRepository.haalPlaatsenOp(this);
|
|
}
|
|
).then(
|
|
function(opgehaaldePlaatsen) {
|
|
opgehaaldePlaatsen.forEach(function(plaats) {
|
|
expect(plaats.geefMeting).toBeDefined();
|
|
});
|
|
}
|
|
);
|
|
});
|
|
});
|
|
```
|
|
|
|
Merk op dat de closure meegeven in `then()` (slechts 1 mogelijk voor assertions) als **argument** het resultaat van de laatste closure in `when()` meekrijgt!
|
|
|
|
#####== jQuery 1.6: Deferred en piping #####==
|
|
|
|
Vanaf **jQuery 1.6** is het mogelijk om met `$.Deferred` te werken, dat het mogelijk maakt om een closure uit te voeren op het moment dat "werk" gedaan is. Met werk bedoelen we:
|
|
|
|
1. fx: `.animate` ea
|
|
2. http: `.ajax` ea
|
|
3. custom code die zelf een `$.Deferred` object retourneren
|
|
|
|
##### Promising stuff #####
|
|
|
|
Alle async operaties worden aan een *queue* toegevoegd van het jQuery element zelf. Je kan op eender welk moment vragen aan dat queue object, dat wanneer alle items zijn verwerkt er iets speciaals uigevoerd wordt:
|
|
|
|
```javascript
|
|
$('#blink').fadeOut().promise().done(function() {
|
|
console.log('done blinking!');
|
|
});
|
|
```
|
|
|
|
Dit kan dus ook met `$.ajax`.
|
|
|
|
##### Zelf Deferred code schrijven #####
|
|
|
|
Maak een deferred object aan door `$.Deferred()` aan te roepen. Op dat moment kan je `done()` hierop zoals in het vorige voorbeeld aanroepen. Bijvoorbeeld:
|
|
|
|
```javascript
|
|
|
|
function startStuff() {
|
|
var df = $.Deferred();
|
|
setTimeout(1000, function() {
|
|
console.log('done async call');
|
|
df.resolve();
|
|
});
|
|
return df.promise();
|
|
}
|
|
|
|
startStuff().done(function() {
|
|
console.log('really really done with "start stuff"!');
|
|
});
|
|
```
|
|
|
|
##### Multiple elements in queue: piping #####
|
|
|
|
Stel dat eerst element #1 animatie triggert, dan #2, en daarna nog logica dient te gebeuren. Dit kan ook met `$.Deferred`, door `.pipe()` te gebruiken om verder te breiden aan de queue.
|
|
|
|
```javascript
|
|
$("button").bind( "click", function() {
|
|
$("p").append( "Started...");
|
|
var div1 ###### $("#div1"), div2 $("#div2");
|
|
|
|
var df = $.Deferred();
|
|
df.pipe(function() {
|
|
return div1.fadeOut("slow")
|
|
}).pipe(function() {
|
|
return div2.fadeOut()
|
|
}).done(function() {
|
|
$("p").append( " -- DONE");
|
|
});
|
|
df.resolve();
|
|
});
|
|
```
|
|
|
|
:exclamation: Pas op, in de `.pipe()` functie moet een nieuw **promised object** geretourneerd worden! Dat nieuw object wordt als return value van de vorige pipe gebruikt. Op die manier wordt er dus *chaining* toegepast. |