brainbaking/content/wiki/code/javascript/async.md

9.3 KiB

+++ title = "async" draft = false tags = [ "code", "javascript", "async" ] date = "2014-07-23" +++

Async coding in JS

Asynchronous programming in JS: APIs interview (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:

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=

$.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

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.

Dit kan op de volgende manier:


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
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:

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!

Los dit op met een utility functie:

flow.sync = function(work) {
  return function() {
    this(work.apply(this, Array.prototype.slice.call(arguments, 0)));
  }
}

Zodat we dit kunnen doen:

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.

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:

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:


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:

$('#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:


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.

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

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.