Event-Based Programming Hva Async har over synkronisering

En av JavaScripts styrker er hvordan den håndterer asynkron (asynk for kort) kode. I stedet for å blokkere tråden blir asynk-kode trykket til en hendelseskø som brenner etter at all annen kode kjøres ut. Det kan imidlertid være vanskelig for nybegynnere å følge asynkoden. Jeg vil bidra til å rydde opp forvirring du måtte ha i denne artikkelen.


Forstå Async-koden

JavaScripts mest grunnleggende async-funksjoner er setTimeout og setInterval. De setTimeout funksjonen utfører en gitt funksjon etter at en viss tid går over. Den aksepterer en tilbakeringingsfunksjon som det første argumentet og en tid (i millisekunder) som det andre argumentet. Her er et eksempel på bruk:

 console.log ("a"); setTimeout (funksjon () console.log ("c"), 500); setTimeout (funksjon () console.log ("d"), 500); setTimeout (funksjon () console.log ("e"), 500); console.log ("b");

Som forventet, viser konsollen utgangene "a", "b" og deretter 500 ms (ish) senere, vi ser "c", "d" og "e". Jeg bruker "ish" fordi setTimeout er faktisk uforutsigbar. Faktisk snakker selv HTML5-spesifikasjonen om dette problemet:

"Denne API-en garanterer ikke at timere vil kjøre nøyaktig på skjema. Forsinkelser på grunn av CPU-belastning, andre oppgaver, osv., Må forventes."

Interessant vil en timeout ikke utføres før alle de resterende kodene i en blokk er utført. Så hvis en tidsavbrudd er satt, og deretter kjører noen lang kjørerfunksjon, vil timeouten ikke engang starte før den lange løpende funksjonen er ferdig. I virkeligheten fungerer asynk som setTimeout og setInterval blir presset på en kø kjent som Event Loop.

De Event Loop er en kø av tilbakeringingsfunksjoner. Når en async-funksjon utføres, blir tilbakeringingsfunksjonen trykket inn i køen. JavaScript-motoren begynner ikke å behandle hendelsesløkken før koden etter en async-funksjon har blitt utført. Dette betyr at JavaScript-koden ikke er multi-threaded, selv om det ser ut til å være slik. Hendelsessløyfen er en første-i-første-ut (FIFO) -kø, noe som betyr at tilbakeringinger kjøres i den rekkefølgen de ble lagt til i køen. JavaScript ble valgt for nodens språk på grunn av hvor lett det er å skrive denne typen kode.


Ajax

Asynkron JavaScript og XML (Ajax) forandret forandret landskapet til JavaScript. Plutselig kan en nettleser oppdatere en nettside uten å måtte laste den på nytt. Koden for implementering av Ajax i forskjellige nettlesere kan være lang og kjedelig å skrive; Men takket være jQuery (og andre biblioteker) ble Ajax en ekstremt enkel og elegant løsning for å lette kundens serverkommunikasjon.

Asynkront henter data med jQuery's $ .ajax er en enkel kryssbrowserprosess, men det er ikke umiddelbart tydelig hva som skjer akkurat bak kulissene. For eksempel:

var data; $ .ajax (url: "noen / url / 1", suksess: funksjon (data) // Men dette vil! console.log (data);) // Ups, dette fungerer ikke ... konsoll .log (data);

Det er vanlig, men feil, å anta at dataene er tilgjengelige umiddelbart etter anrop $ .ajax, men det som faktisk skjer er dette:

xmlhttp.open ("GET", "some / ur / 1", true); xmlhttp.onreadystatechange = funksjon (data) if (xmlhttp.readyState === 4) console.log (data); ; xmlhttp.send (null);

Den underliggende XMLHttpRequest (XHR) -objektet sender forespørselen, og tilbakeringingsfunksjonen er innstilt for å håndtere XHR-ene readystatechange begivenhet. Så XHR er sende metode utfører. Som XHR utfører sitt arbeid, en intern readystatechange hendelsen bryr hver gang readyState Egenskapsendringer, og det er først når XHR er ferdig med å motta et svar fra den eksterne verten som tilbakeringingsfunksjonen utfører.


Arbeider med Async-kode

Async programmering egner seg til det som ofte kalles "callback hell". Fordi nesten alle asynkfunksjoner i JavaScript bruker tilbakeringinger, utfører flere sekvensielle asynkfunksjoner mange innledede tilbakekallinger - noe som resulterer i vanskelig å lese kode.

Mange av funksjonene i node.js er asynk. Så, kode som følgende er ganske vanlig.

var fs = krever ("fs"); fs.eksisterer ("index.js", funksjon () fs.readFile ("index.js", "utf8", funksjon (feil, innhold) contents = someFunction (innhold); // gjør noe med innholdet fs. writeFile ("index.js", "utf8", funksjon () console.log ("whew! Klar til slutt ...");;;;;); console.log ("kjører ...");

Det er også vanlig å se klientens sidekode, som følgende:

GMaps.geocode (adresse: fromAddress, tilbakeringing: funksjon (resultater, status) if (status == "OK") fromLatLng = resultater [0] .geometry.location; GMaps.geocode (adresse: toAddress, tilbakeringing: funksjon (resultater, status) if (status == "OK") toLatLng = resultater [0] .geometry.location; map.getRoutes (opprinnelse: [fromLatLng.lat (), fromLatLng.lng ()], destinasjon : [toLatLng.lat (), toLatLng.lng ()], travelMode: "driving", unitSystem: "imperial", tilbakeringing: funksjon (e) console.log ("ANNNEND FINALLY her er retningene ..."); // gjør noe med e);););

Nested callbacks kan bli veldig ekkel, men det finnes flere løsninger på denne typen koding.

Problemet er ikke med språket selv; Det er med måten programmerere bruker språket - Async Javascript.

Navngitte funksjoner

En enkel løsning som renser nestede tilbakeringinger, er å unngå å nesting mer enn to nivåer. I stedet for å sende anonyme funksjoner til tilbakekallingsargumentene, send en navngitt funksjon:

var fraLatLng, toLatLng; var routeDone = funksjon (e) console.log ("ANNNND ENDELIG her er retningene ..."); // gjør noe med e; var toAddressDone = funksjon (resultater, status) if (status == "OK") toLatLng = results [0] .geometry.location; map.getRoutes (opprinnelse: [fromLatLng.lat (), fromLatLng.lng ()], destinasjon: [toLatLng.lat (), toLatLng.lng ()], travelMode: "driving", unitSystem: "imperial" : routeDone); ; var fromAddressDone = funksjon (resultater, status) if (status == "OK") fromLatLng = results [0] .geometry.location; GMaps.geocode (adresse: toAddress, tilbakeringing: toAddressDone); ; GMaps.geocode (adresse: fromAddress, tilbakeringing: fromAddressDone);

I tillegg kan async.js-biblioteket håndtere flere Ajax-forespørsler / svar. For eksempel:

async.parallel ([funksjon (ferdig) GMaps.geocode (adresse: toAddress, tilbakeringing: funksjon (resultat) ferdig (null, resultat););, funksjon (ferdig) GMaps.geocode : fraAddress, tilbakeringing: funksjon (resultat) ferdig (null, resultat););], funksjon (feil, resultater) getRoute (resultater [0], resultater [1]););

Denne koden utfører de to asynkrone funksjonene, og hver funksjon aksepterer en "ferdig" tilbakeringing som utføres etter at async-funksjonen er ferdig. Når begge "ferdige" tilbakeringinger er ferdig, vil parallell funksjonens tilbakeringing utfører og håndterer eventuelle feil eller resultater fra de to async-funksjonene.

Promises

Fra CommonJS / A:

Et løfte representerer den endelige verdien returnert fra den endelige gjennomføringen av en operasjon.

Det er mange biblioteker som inneholder løfte mønsteret, og jQuery-brukere har allerede et godt løfte API tilgjengelig for dem. jQuery introduserte Utsatt objekt i versjon 1.5, og bruke jQuery.Deferred Konstruktør resulterer i en funksjon som gir et løfte. En løfteavkastende funksjon utfører en slags async-operasjon og løser oppsigelsen ved ferdigstillelse.

var geocode = funksjon (adresse) var dfd = ny $ .Deferred (); GMaps.geocode (adresse: adresse, tilbakeringing: funksjon (svar, status) return dfd.resolve (respons);); returnere dfd.promise (); ; var getRoute = funksjon (fromLatLng, toLatLng) var dfd = new $ .Deferred (); map.getRoutes (opprinnelse: [fromLatLng.lat (), fromLatLng.lng ()], destinasjon: [toLatLng.lat (), toLatLng.lng ()], travelMode: "driving", unitSystem: "imperial" : funksjon (e) return dfd.resolve (e);); returnere dfd.promise (); ; var doSomethingCoolWithDirections = funksjon (rute) // gjør noe med rute; $ .when (geocode (fromAddress), geocode (toAddress)). da (funksjon (fromLatLng, toLatLng) getRoute (fromLatLng, toLatLng) .then (doSomethingCoolWithDirections););

Dette lar deg utføre to asynkroniske funksjoner, vente på resultatene, og utfør deretter en annen funksjon med resultatene fra de første to samtalene.

Et løfte representerer den endelige verdien returnert fra den endelige gjennomføringen av en operasjon.

I denne koden er geokode Metoden kjører to ganger og gir et løfte. Async-funksjonene utfører og ringer deretter Løse i sine tilbakekallinger. Så, når begge har ringt Løse, de deretter utfører, returnerer resultatene av de to første anropene til geokode. Resultatene sendes deretter til getRoute, som også returnerer et løfte. Til slutt, når løftet fra getRoute er løst, den doSomethingCoolWithDirections tilbakeringing kjøres.

arrangementer

Hendelser er en annen løsning for å kommunisere når async tilbakekallinger er ferdigstilt. Et objekt kan bli en emitter og publisere hendelser som andre objekter kan lytte til. Denne typen begivenhet kalles observatørmønster. Backbone.js biblioteket har denne typen funksjonalitet bygget inn med Backbone.Events.

var SomeModel = Backbone.Model.extend (url: "/ someurl"); var SomeView = Backbone.View.extend (initialiser: funksjon () this.model.on ("reset", this.render, dette); this.model.fetch ();, render: funksjon (data)  // gjør noe med data); var view = new SomeView (modell: new SomeModel ());

Det finnes andre mixin-eksempler og biblioteker for utgivelse av hendelser, for eksempel jQuery Event Emitter, EventEmitter, monologue.js og node.js har en innebygd EventEmitter-modul.

Event Loop er en kø av tilbakeringingsfunksjoner.

En lignende metode for publisering av meldinger bruker mediator mønster, brukes i posten biblioteket. I mediatormønsteret hører en mellommann for alle objekter til og publiserer hendelser. I denne tilnærmingen har ikke en gjenstand en direkte referanse til en annen, og derved kobler objektene fra hverandre.

Aldri returnere et løfte over en offentlig API. Dette forbinder API-forbrukerne med å bruke løfter og gjør refactoring vanskelig. En kombinasjon av løfter for interne formål og eventing for eksterne APIer kan imidlertid føre til en pent dekoblet og testbar app.

I det forrige eksempelet, doSomethingCoolWithDirections Tilbakeringingsfunksjonen utføres når de to foregående geokode funksjoner har fullført. De doSomethingCoolWithDirections kan da ta svaret det mottok fra getRoute og publiser svaret som en melding.

var doSomethingCoolWithDirections = funksjon (rute) postal.channel ("ui") .publish ("directions.done", rute: rute); ;

Dette tillater andre områder av applikasjonen å svare på den asynkrone tilbakeringingen uten å ha en direkte referanse til forespørselsobjektet. Det er mulig at flere områder på en side må oppdateres når retninger er hentet. I et typisk jQuery Ajax-oppsett vil suksess tilbakeringingen måtte justeres når du mottar en endring av retninger. Dette kan bli vanskelig å vedlikeholde, men ved å bruke meldinger, er det mye lettere å oppdatere flere deler av brukergrensesnittet.

var UI = funksjon () this.channel = postal.channel ("ui"); this.channel.subscribe ("directions.done", this.updateDirections) .withContext (dette); ; UI.prototype.updateDirections = funksjon (data) // Ruten er tilgjengelig på data.route, oppdater nå bare brukergrensesnittet; app.ui = nytt brukergrensesnitt ();

Noen andre mediatormønsterbaserte meldingsbiblioteker forsterkes, PubSubJS og radio.js.


Konklusjon

JavaScript gjør det svært enkelt å skrive async-kode. Ved hjelp av løfter, eventing eller navngitte funksjoner eliminerer den ekle "callback hell". For mer informasjon om async JavaScript, kassen Async JavaScript: Bygg mer responsive apper med mindre kode. Mange av eksemplene fra innlegget ligger i et Github-lager kalt NetTutsAsyncJS. Klone bort!