Se hai seguito il mondo di JavaScript, probabilmente avrai sentito parlare di promesse. Ci sono alcuni fantastici tutorial online se vuoi conoscere le promesse, ma non le spiegherò qui; questo articolo presuppone che tu abbia già una conoscenza pratica delle promesse.
Le promesse sono propagandate come il futuro della programmazione asincrona in JavaScript. Le promesse sono davvero grandi e aiutano a risolvere un sacco di problemi che sorgono con la programmazione asincrona, ma questa affermazione è solo in qualche modo corretta. In realtà, le promesse sono le fondazione del futuro della programmazione asincrona in JavaScript. Idealmente, le promesse saranno nascoste dietro le quinte e saremo in grado di scrivere il nostro codice asincrono come se fosse sincrono.
In ECMAScript 7, questo diventerà più di un sogno fantasioso: diventerà realtà, e ti mostrerò quella realtà chiamata funzioni asincrone, proprio ora. Perché stiamo parlando di questo ora? Dopotutto, ES6 non è nemmeno stato completamente finalizzato, quindi chissà quanto tempo ci vorrà prima di vedere ES7. La verità è che puoi usare questa tecnologia proprio adesso, e alla fine di questo post, ti mostrerò come.
Prima di iniziare a dimostrare come utilizzare le funzioni asincrone, voglio passare alcuni esempi con promesse (usando le promesse ES6). Più tardi, convertirò questi esempi per utilizzare le funzioni asincrone in modo da poter vedere che grande differenza fa.
Per il nostro primo esempio, faremo qualcosa di veramente semplice: chiamare una funzione asincrona e registrare il valore restituito.
function getValues () return Promise.resolve ([1,2,3,4]); getValues (). then (function (values) console.log (valori););
Ora che abbiamo definito questo esempio di base, passiamo a qualcosa di un po 'più complicato. Userò e modificherò esempi da un post sul mio blog che descrive alcuni schemi per l'uso di promesse in diversi scenari. Ciascuno degli esempi recupera in modo asincrono un array di valori, esegue un'operazione asincrona che trasforma ogni valore nell'array, registra ogni nuovo valore e infine restituisce l'array riempito con i nuovi valori.
Innanzitutto, esamineremo un esempio che eseguirà più operazioni asincrone in parallelo, quindi risponderemo immediatamente a ciascuna di esse, indipendentemente dall'ordine in cui terminano. Il getValues
la funzione è la stessa dell'esempio precedente. Il asyncOperation
la funzione verrà anche riutilizzata negli esempi successivi.
function asyncOperation (value) return Promise.resolve (valore + 1); function foo () return getValues (). then (function (values) var operations = values.map (function (value) return asyncOperation (value) .then (function (newValue) console.log (newValue); return newValue;);); return Promise.all (operations);). catch (function (err) console.log ('Abbiamo avuto un', err););
Possiamo fare esattamente la stessa cosa, ma assicurarci che la registrazione avvenga nell'ordine degli elementi dell'array. In altre parole, questo prossimo esempio farà il lavoro asincrono in parallelo, ma il lavoro sincrono sarà sequenziale:
function foo () return getValues (). then (function (values) var operations = values.map (asyncOperation); return Promise.all (operations) .then (function (newValues) newValues.forEach (function (newValue) console.log (newValue);); return newValues;);). catch (function (err) console.log ('Abbiamo avuto un', err););
Il nostro esempio finale mostrerà un pattern in cui attendiamo che una precedente operazione asincrona finisca prima di iniziare quella successiva. In questo esempio non c'è nulla che funzioni in parallelo; tutto è sequenziale.
function foo () var newValues = []; return getValues (). then (function (values) return values.reduce (function (previousOperation, value) return previousOperation.then (function () restituisce asyncOperation (valore);). then (function (newValue) console .log (newValue); newValues.push (newValue););, Promise.resolve ()). then (function () return newValues;);). catch (function (err) console.log ('Abbiamo avuto un', errare););
Anche con la capacità delle promesse di ridurre il nesting del callback, non aiuta molto. L'esecuzione di un numero sconosciuto di chiamate sequenziali asincrone sarà disordinata, qualunque cosa tu faccia. È particolarmente spaventoso vedere tutti quelli nidificati ritorno
parole chiave. Se abbiamo passato il newValues
schierarsi attraverso le promesse in ridurre
Il callback invece di renderlo globale all'intero foo
funzione, avremmo bisogno di regolare il codice per avere ritorni più annidati, come questo:
function foo () return getValues (). then (function (values) return values.reduce (function (previousOperation, value) return previousOperation.then (function (newValues) return asyncOperation (value) .then (function (newValue ) console.log (newValue); newValues.push (newValue); return newValues;););, Promise.resolve ([]));). catch (function (err) console.log ( 'Abbiamo avuto un', errare););
Non sei d'accordo che dobbiamo risolvere questo problema? Diamo un'occhiata alla soluzione.
Anche con le promesse, la programmazione asincrona non è esattamente semplice e non scorre sempre bene dalla A alla Z. La programmazione sincrona è molto più semplice e viene scritta e letta in modo molto più naturale. Le specifiche delle funzioni asincrone cercano in un modo (usando i generatori ES6 dietro le quinte) di scrivere il codice come se fosse sincrono.
La prima cosa che dobbiamo fare è prefisso le nostre funzioni con il async
parola chiave. Senza questa parola chiave, non possiamo usare l'importantissimo attendere
parola chiave all'interno di quella funzione, che spiegherò tra un po '.
Il async
parola chiave non solo ci permette di usare attendere
, garantisce inoltre che la funzione restituisca a Promettere
oggetto. All'interno di una funzione asincrona, in qualsiasi momento ritorno
un valore, la funzione lo farà in realtà ritorno a Promettere
questo è risolto con quel valore. Il modo per rifiutare è lanciare un errore, nel qual caso il valore di rifiuto sarà l'oggetto errore. Ecco un semplice esempio:
async function foo () if (Math.round (Math.random ())) restituisce 'Success!'; altrimenti lancia "Fallimento!"; // È equivalente a ... function foo () if (Math.round (Math.random ())) restituisce Promise.resolve ('Success!'); else return Promise.reject ('Failure!');
Non abbiamo nemmeno ottenuto la parte migliore e abbiamo già reso il nostro codice più simile al codice sincrono perché siamo stati in grado di smettere di fare scherzi espliciti con il Promettere
oggetto. Possiamo assumere qualsiasi funzione e farla restituire a Promettere
oggetto solo aggiungendo il async
parola chiave in primo piano.
Andiamo avanti e convertiamo i nostri getValues
e asyncOperation
funzioni:
funzione asincrona getValues () return [1,2,3,4]; async function asyncOperation (value) return value + 1;
Facile! Ora, diamo un'occhiata alla parte migliore di tutti: il attendere
parola chiave. All'interno della tua funzione asincrona, ogni volta che esegui un'operazione che restituisce una promessa, puoi lanciare il attendere
parola chiave di fronte ad esso, e smetterà di eseguire il resto della funzione fino a quando la promessa restituita non sarà stata risolta o respinta. A quel punto, il Attendere promisingOperation ()
valuterà il valore risolto o rifiutato. Per esempio:
function promisingOperation () return new Promise (function (resolve, reject) setTimeout (function () if (Math.round (Math.random ())) risolve ('Success!'); else respinge ('Failure!') );, 1000); funzione async foo () var message = attende promisingOperation (); console.log (message);
Quando chiami foo
, o aspetterà fino a quando promisingOperation
risolve e poi si disconnetterà il "Successo!" messaggio, o promisingOperation
rifiuterà, nel qual caso il rifiuto sarà passato attraverso e foo
rifiuterà con "Failure!". Da foo
non restituisce nulla, si risolverà con non definito
supponendo promisingOperation
ha successo.
Rimane solo una domanda: come risolviamo i fallimenti? La risposta a questa domanda è semplice: tutto ciò che dobbiamo fare è racchiuderlo in a prova a prendere
bloccare. Se una delle operazioni asincrone viene rifiutata, possiamo catturare
e gestirlo:
async function foo () try var message = attende promisingOperation (); console.log (messaggio); catch (e) console.log ('Abbiamo fallito:', e);
Ora che abbiamo raggiunto tutte le nozioni di base, esaminiamo i nostri precedenti esempi di promessa e li convertiamo in funzioni asincrone.
Il primo esempio sopra creato getValues
e l'ho usato Abbiamo già ricreato getValues
quindi abbiamo solo bisogno di ricreare il codice per usarlo. C'è un potenziale avvertimento per le funzioni asincrone che appare qui: Il codice è necessario essere in una funzione L'esempio precedente riguardava l'ambito globale (per quanto chiunque potrebbe dirlo), ma abbiamo bisogno di avvolgere il nostro codice asincrono in una funzione asincrona per farlo funzionare:
async function () console.log (attendi getValues ()); (); // L'extra "()" esegue immediatamente la funzione
Anche con il wrapping del codice in una funzione, continuo a sostenere che è più facile da leggere e ha meno byte (se rimuovi il commento). Il nostro prossimo esempio, se ricordi correttamente, fa tutto in parallelo. Questo è un po 'complicato, perché abbiamo una funzione interiore che deve restituire una promessa. Se stiamo usando il attendere
parola chiave all'interno della funzione interna, anche questa funzione deve essere preceduta da async
.
funzione async foo () try var values = attende getValues (); var newValues = values.map (funzione async (valore) var newValue = attende asyncOperation (valore); console.log (newValue); return newValue;); return await * newValues; catch (err) console.log ('Abbiamo avuto un', err);
Potresti aver notato l'asterisco allegato all'ultima attendere
parola chiave. Questo sembra essere ancora in discussione un po ', ma sembra await *
essenzialmente avvolgerà automaticamente l'espressione alla sua destra in Promise.all
. In questo momento, però, lo strumento che vedremo più avanti non supporta await *
, quindi dovrebbe essere convertito in attendere Promise.all (newValues);
come stiamo facendo nel prossimo esempio.
Il prossimo esempio sparerà il asyncOperation
chiama in parallelo, ma poi riavvia tutto e fa l'output in sequenza.
funzione async foo () try var values = attende getValues (); var newValues = attendi Promise.all (values.map (asyncOperation)); newValues.forEach (funzione (valore) console.log (valore);); return newValues; catch (err) console.log ('Abbiamo avuto un', err);
Lo amo. È estremamente pulito. Se abbiamo rimosso il attendere
e async
parole chiave, rimosso il Promise.all
involucro e fatto getValues
e asyncOperation
sincrono, quindi questo codice funzionerebbe ancora allo stesso modo, tranne che sarebbe sincrono. Questo è essenzialmente ciò che intendiamo raggiungere.
Il nostro ultimo esempio, ovviamente, avrà tutto in sequenza. Nessuna operazione asincrona viene eseguita fino al completamento della precedente.
funzione async foo () try var values = attende getValues (); return await values.reduce (funzione async (valori, valore) values = attende valori; valore = attende asyncOperation (valore); console.log (valore); values.push (valore); valori di ritorno;, []); catch (err) console.log ('Abbiamo avuto un', err);
Ancora una volta, stiamo facendo una funzione interiore async
. C'è un bizzarro interessante rivelato in questo codice. Passai []
in come il valore "memo" a ridurre
, ma poi ho usato attendere
su di esso. Il valore a destra di attendere
non è richiesto di essere una promessa. Può assumere qualsiasi valore e, se non è una promessa, non lo aspetterà; sarà eseguito solo in modo sincrono. Naturalmente, tuttavia, dopo la prima esecuzione del callback, lavoreremo effettivamente con una promessa.
Questo esempio è praticamente come il primo esempio, tranne che stiamo usando ridurre
invece di carta geografica
così che possiamo attendere
l'operazione precedente, e poi perché stiamo usando ridurre
per costruire un array (non qualcosa che normalmente faresti, specialmente se stai costruendo un array delle stesse dimensioni dell'array originale), abbiamo bisogno di costruire l'array all'interno del callback per ridurre
.
Ora che hai intravisto la semplicità e la bellezza delle funzioni asincrone, potresti piangere come ho fatto la prima volta che le ho viste. Non stavo piangendo di gioia (anche se l'ho quasi fatto); no, stavo piangendo perché ES7 non sarà qui finché non morirò! Almeno è così che io provato. Poi ho scoperto di Traceur.
Traceur è scritto e gestito da Google. È un transpiler che converte il codice ES6 in ES5. Questo non aiuta! Beh, non lo farebbe, tranne che hanno anche implementato il supporto per le funzioni asincrone. È ancora una funzione sperimentale, il che significa che dovrai dire esplicitamente al compilatore che stai usando quella funzione e che vorrai sicuramente testare il tuo codice per assicurarti che non ci siano problemi con la compilazione.
Usando un compilatore come Traceur significa che avrai un po 'di codice brutto, un po' gonfio, che viene inviato al client, che non è quello che vuoi, ma se usi le mappe sorgente, questo essenzialmente elimina la maggior parte dei svantaggi legati allo sviluppo. Stai leggendo, scrivendo e eseguendo il debug di un codice ES6 / 7 pulito, piuttosto che dover leggere, scrivere ed eseguire il debug di un caos di codice complesso che deve aggirare i limiti della lingua.
Naturalmente, la dimensione del codice sarà ancora maggiore di quella che avresti se avessi scritto a mano il codice ES5 (molto probabilmente), quindi potresti aver bisogno di trovare un qualche tipo di bilancia tra codice mantenibile e codice performante, ma questo è un equilibrio di cui hai spesso bisogno per trovare anche senza usare un transpiler.
Traceur è un'utilità da riga di comando che può essere installata tramite NPM:
npm install -g traceur
In generale, Traceur è abbastanza semplice da usare, ma alcune delle opzioni possono essere confuse e potrebbero richiedere alcuni esperimenti. Puoi vedere un elenco delle opzioni per maggiori dettagli. Quello a cui siamo veramente interessati è il --sperimentale
opzione.
È necessario utilizzare questa opzione per abilitare le funzionalità sperimentali, che è il modo in cui funzionano le funzioni asincrone. Una volta che hai un file JavaScript (main.js
in questo caso) con il codice ES6 e le funzioni asincrone incluse, puoi semplicemente compilarlo con questo:
traceur main.js --experimental --out compiled.js
Puoi anche solo eseguire il codice omettendo il --fuori compilated.js
. Non vedrai molto a meno che il codice non abbia console.log
istruzioni (o altri output della console), ma per lo meno è possibile verificare la presenza di errori. Probabilmente vorrai eseguirlo in un browser, però. Se è così, ci sono ancora alcuni passi da fare.
traceur-runtime.js
script. Ci sono molti modi per ottenerlo, ma uno dei più facili è da NPM: npm installa traceur-runtime
. Il file sarà quindi disponibile come index.js
all'interno della cartella di quel modulo.copione
tag per inserire lo script Traceur Runtime.copione
etichetta sotto lo script Traceur Runtime da inserire compiled.js
.Dopo questo, il tuo codice dovrebbe essere attivo e funzionante!
Oltre all'utilizzo dello strumento da riga di comando Traceur, è anche possibile automatizzare la compilation in modo da non dover tornare alla console e rieseguire il compilatore. Grunt and Gulp, che sono task run automatizzati, hanno ciascuno i propri plugin che puoi usare per automatizzare la compilazione di Traceur: grunt-traceur e gulp-traceur rispettivamente.
Ognuno di questi task runner può essere configurato per guardare il tuo file system e ricompilare il codice nel momento in cui salvi le modifiche ai tuoi file JavaScript. Per sapere come usare Grunt o Gulp, controlla la documentazione di "Come iniziare".
Le funzioni asincrone di ES7 offrono agli sviluppatori un modo per farlo in realtà uscire dall'inferno del callback in un modo che le promesse non potrebbero mai fare da sole. Questa nuova funzionalità ci consente di scrivere codice asincrono in modo estremamente simile al nostro codice sincrono e, sebbene ES6 sia ancora in attesa del suo rilascio completo, possiamo già utilizzare le funzioni asincrone oggi attraverso la transpilation. Che cosa state aspettando? Esci e rendi il tuo codice fantastico!