Se hai appena iniziato con la programmazione, è probabile che tu stia pensando ai programmi come a un insieme di blocchi logici sequenziali, in cui ogni blocco fa una cosa specifica e trasmette il suo risultato in modo che il blocco successivo possa essere eseguito e così via, e per il resto nella maggior parte dei casi hai ragione, la maggior parte dei programmi viene eseguita in modo sequenziale, questo modello ci consente di creare programmi semplici da scrivere e mantenere. Esistono tuttavia casi d'uso specifici in cui questo modello sequenziale non funzionerebbe o non sarebbe ottimale. Ad esempio, considera un'applicazione per la lettura di libri. Questa applicazione ha alcune funzionalità avanzate come la ricerca di tutte le occorrenze di una parola, la navigazione tra i segnalibri e simili. Immaginiamo ora che l'utente stia leggendo un lungo libro e decida di cercare tutte le occorrenze di una parola comune come "The". L'applicazione normalmente impiega un paio di secondi per trovare e indicizzare tutte le occorrenze di quella parola. In un programma sequenziale l'utente non può interagire con l'applicazione (cambiare pagina o evidenziare un testo) finché non viene completata l'operazione di ricerca. Spero che tu possa vedere che non è un’esperienza utente ottimale!
Il diagramma illustra un tipico flusso di esecuzione dell'applicazione di lettura libri. Se l'utente avvia un'operazione di lunga durata (in questo caso la ricerca di tutte le occorrenze di “the” in un libro di grandi dimensioni), l'applicazione si “blocca” per tutta la durata di tale operazione. In questo caso, l'utente continuerà a fare clic sul pulsante del segnalibro successivo senza alcun risultato fino al termine dell'operazione di ricerca e tutte le operazioni avranno effetto contemporaneamente dando all'utente finale la sensazione di un'applicazione in ritardo.
Potresti aver notato che questo esempio non corrisponde realmente al modello sequenziale che abbiamo introdotto in precedenza. Questo perché le operazioni qui sono indipendenti l'una dall'altra. L’utente non ha bisogno di conoscere il numero di occorrenze di “the” per passare al segnalibro successivo, quindi l’ordine di esecuzione delle operazioni non è molto importante. Non dobbiamo attendere la fine dell'operazione di ricerca prima di poter passare al segnalibro successivo. Un possibile miglioramento al flusso di esecuzione precedente si basa su questa logica: possiamo eseguire l'operazione di ricerca lunga in background, procedere con le eventuali operazioni in entrata e, una volta terminata l'operazione lunga, possiamo semplicemente avvisare l'utente. Il flusso di esecuzione diventa il seguente:
Con questo flusso di esecuzione, l'esperienza dell'utente è notevolmente migliorata. Ora l'utente può avviare un'operazione di lunga durata, procedere a utilizzare normalmente l'applicazione e ricevere una notifica al termine dell'operazione. Questa è la base della programmazione asincrona.
Javascript, tra gli altri linguaggi, supporta questo stile di programmazione asincrona fornendo API estese per ottenere praticamente qualsiasi comportamento asincrono a cui puoi pensare. Alla fine, Javascript dovrebbe essere intrinsecamente un linguaggio asincrono. Se facciamo riferimento all'esempio precedente, la logica asincrona è alla base di tutte le applicazioni di interazione con l'utente e Javascript è stato creato principalmente per essere utilizzato sul browser dove la maggior parte dei programmi risponde alle azioni dell'utente.
Quanto segue ti fornirà una breve guida su Javascript asincrono:
Richiamate
In un tipico programma, di solito troverai una serie di funzioni. Per utilizzare una funzione, la chiamiamo con una serie di parametri. Il codice della funzione verrà eseguito e restituirà un risultato, niente di straordinario. La programmazione asincrona sposta leggermente questa logica. Tornando all'esempio dell'applicazione di lettura del libro, non possiamo utilizzare una funzione normale per implementare la logica dell'operazione di ricerca poiché l'operazione richiede una quantità di tempo sconosciuta. Una funzione regolare ritornerà sostanzialmente prima che l'operazione venga completata e questo non è il comportamento che ci aspettiamo. La soluzione è specificare un'altra funzione che verrà eseguita una volta completata l'operazione di ricerca. Questo modella il nostro caso d'uso poiché il nostro programma può continuare il suo flusso normalmente e una volta terminata l'operazione di ricerca, la funzione specificata verrà eseguita per notificare all'utente i risultati della ricerca. Questa funzione è ciò che chiamiamo funzione di callback:
// Search occurrences function
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in result variable
//....
callback(null, word, result);
} catch (err) {
callback(err);
}
}
// Search occurrences callback function
function handleSearchOccurrencesResult(err, word, result) {
if (err) {
console.log(`Search operation for ${word} ended with an error`);
} else console.log(`Search results for ${word}: ${result}`);
return;
}
searchOccurrences("the", handleSearchOccurrencesResult);
Per prima cosa definiamo la funzione dell'operazione di ricerca, searchOccurrences. Richiede la parola da cercare ed un secondo parametro “callback” che sarà la funzione da eseguire una volta terminata l'operazione di ricerca. La funzione dell'operazione di ricerca è stata volutamente mantenuta astratta, dobbiamo solo concentrarci sui suoi due possibili esiti: nel primo caso tutto è andato a buon fine e abbiamo il risultato della ricerca nella variabile risultato. In questo caso non ci resta che chiamare la funzione callback con i seguenti parametri: il primo parametro è null ovvero non si è verificato alcun errore, il secondo parametro è la parola cercata e il terzo e forse il più importante parametro dei tre, è il risultato dell'operazione di ricerca.
Il secondo caso è quello in cui si verifica un errore, anche questo è il caso in cui viene eseguita l'operazione di ricerca e dobbiamo chiamare la funzione di callback. Usiamo un blocco try and catch per intercettare qualsiasi errore e chiamiamo semplicemente la funzione callback con l'oggetto error dal blocco catch.
Abbiamo quindi definito la funzione di callback, handleSearchOccurrences, mantenendo la sua logica abbastanza semplice. È solo questione di stampare un messaggio sulla console. Per prima cosa controlliamo il parametro “err” per vedere se si è verificato qualche errore nella funzione principale. In tal caso, comunichiamo semplicemente all'utente che l'operazione di ricerca si è conclusa con un errore. Se non sono stati segnalati errori, stampiamo un messaggio con il risultato dell'operazione di ricerca.
Infine, chiamiamo la funzione searchOccurrences con la parola “the”. La funzione ora verrà eseguita normalmente senza bloccare il programma principale e una volta terminata la ricerca, verrà eseguito il callback e otterremo il messaggio di risultato con il risultato della ricerca o il messaggio di errore.
È importante menzionare qui che abbiamo accesso solo alla variabile risultato all'interno delle funzioni main e callback. Se proviamo qualcosa del genere:
let result;
function searchOccurrences(word, callback) {
try {
// search operation logic, result is in searchResult variable
//....
result = searchResult;
callback(null, word, result);
} catch (err) {
callback(err);
}
}
searchOccurrences("the", handleSearchOccurrencesResult);
console.log(result);
il risultato della stampa sarebbe indefinito perché i programmi non aspettano l'esecuzione della funzione searchOccurrences. Passa all'istruzione successiva che è l'istruzione print prima che la variabile risultato venga assegnata all'interno della funzione principale. Di conseguenza, stamperemo la variabile risultato non assegnata.
Quindi, in base a questa logica, dovremmo mantenere tutto il codice che utilizza la variabile risultato all'interno della funzione di callback. Questo potrebbe non sembrare un problema ora, ma potrebbe rapidamente trasformarsi in un problema reale. Immagina il caso in cui abbiamo una catena di funzioni asincrone che devono essere eseguite in sequenza. Nella tipica logica di callback, implementeresti qualcosa del genere:
functionA(function (err, resA) {
///......
functionB(resA, function (err, resB) {
///......
functionC(resB, function (err, resC) {
///......
functionD(resC, function (err, resD) {
///......
});
});
});
});
Tieni presente che ogni callback ha un parametro di errore e ogni errore deve essere gestito separatamente. Ciò rende il codice già complesso di cui sopra ancora più complesso e difficile da mantenere. Se tutto va bene, Promises è qui per risolvere il problema del callback, ne parleremo in seguito.
Promesse
Le promesse si basano sui richiami e funzionano in modo simile. Sono stati introdotti come parte delle funzionalità ES6 per risolvere alcuni dei problemi evidenti con i callback come l'inferno dei callback. Le promesse forniscono le proprie funzioni che vengono eseguite in caso di completamento positivo (risolvi) e quando si verificano errori (rifiuta). Di seguito viene illustrato l'esempio searchOccurrences implementato con le promesse:
// Search occurrences function
function searchOccurrences(word) {
return new Promise((resolve, reject) => {
try {
// search operation logic, result is in result variable
//....
resolve(word, result);
} catch (err) {
reject(err);
}
});
}
searchOccurrences("the")
.then((word, result) => {
console.log(`Search results for ${word}: ${result}`);
})
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Esaminiamo le modifiche che abbiamo applicato:
La funzione searchOccurrences restituisce una promessa. All'interno della promessa, manteniamo la stessa logica: abbiamo due funzioni risolvere e rifiutare che rappresentano i nostri callback invece di avere un'unica funzione di callback che gestisce sia un'esecuzione riuscita che un'esecuzione con errori. Le promesse separano i due risultati e forniscono una sintassi pulita quando si chiama la funzione main. La funzione di risoluzione è “agganciata” alla funzione principale utilizzando la parola chiave “then”. Qui specifichiamo semplicemente i due parametri della funzione di risoluzione e stampiamo il risultato della ricerca. Una cosa simile vale per la funzione di rifiuto, può essere agganciata utilizzando la parola chiave “catch”. Si spera che tu possa apprezzare i vantaggi offerti dalle promesse in termini di leggibilità e pulizia del codice. Se ne stai ancora discutendo, controlla come possiamo risolvere il problema del callback concatenando insieme le funzioni asincrone per eseguirle una dopo l'altra:
searchOccurrences("the")
.then(searchOccurrences("asynchronous"))
.then(searchOccurrences("javascript"))
.then(searchOccurrences("guide"))
.catch((err) => {
console.log(`Search operation ended with an error`);
});
Asincrono/Aspetta
Async/Await sono l'ultima aggiunta alla nostra cintura di strumenti asincroni in Javascript. Introdotti con ES8, forniscono un nuovo livello di astrazione oltre alle funzioni asincrone semplicemente "attendendo" l'esecuzione di un'operazione asincrona. Il flusso del programma si blocca in corrispondenza di quell'istruzione finché non viene restituito un risultato dall'operazione asincrona, quindi il programma procederà con l'istruzione successiva. Se stai pensando al flusso di esecuzione sincrono, hai ragione. Abbiamo chiuso il cerchio! Async/await tenta di portare la semplicità della programmazione sincrona nel mondo asincrono. Tieni presente che questo si percepisce solo nell'esecuzione e nel codice del programma. Tutto rimane uguale sotto il cofano, Async/await utilizzano ancora le promesse e i callback sono i loro elementi costitutivi.
Esaminiamo il nostro esempio e implementiamolo utilizzando Async/await:
async function searchOccurrences(word) {
try {
// search operation logic, result is in result variable
//....
return result;
} catch (err) {
console.log(`Search operation for ${word} ended with an error`);
}
}
const word = "the";
const result = await searchOccurrences(word, handleSearchOccurrencesResult);
console.log(`Search results for ${word}: ${result}`);
Il nostro codice non è cambiato molto, la cosa importante da notare qui è la parola chiave "async" prima della dichiarazione della funzione searchOccurrences. Ciò indica che la funzione è asincrona. Inoltre, nota la parola chiave "await" quando chiami la funzione searchOccurrences. Ciò istruirà il programma ad attendere l'esecuzione della funzione fino alla restituzione del risultato prima che il programma possa passare all'istruzione successiva, in altre parole, la variabile risultato manterrà sempre il valore restituito dalla funzione searchOccurrences e non la promessa di la funzione, in questo senso, Async/Await non ha uno stato in sospeso come Promises. Una volta terminata l'esecuzione, passiamo all'istruzione print e questa volta il risultato contiene effettivamente il risultato dell'operazione di ricerca. Come previsto, il nuovo codice ha lo stesso comportamento come se fosse sincrono.
Un'altra cosa minore da tenere a mente è che, poiché non abbiamo più funzioni di callback, dobbiamo gestire l'errore searchOccurrences all'interno della stessa funzione poiché non possiamo semplicemente propagare l'errore alla funzione di callback e gestirlo lì. Qui stiamo semplicemente stampando un messaggio di errore in caso di errore per motivi di esempio.
Incartare
In questo articolo abbiamo esaminato i diversi approcci utilizzati per implementare la logica asincrona in Javascript. Abbiamo iniziato esplorando un esempio concreto del motivo per cui avremmo bisogno di passare dal normale stile di programmazione sincrono al modello asincrono. Siamo poi passati ai callback, che sono gli elementi costitutivi principali del Javascript asincrono. Le limitazioni dei callback ci hanno portato alle diverse alternative che sono state aggiunte nel corso degli anni per superare queste limitazioni, principalmente promesse e Async/await. La logica asincrona può essere trovata ovunque sul Web, sia che si chiami un'API esterna, si avvii una query sul database, si scriva sul filesystem locale o addirittura si attenda l'input dell'utente su un modulo di accesso. Si spera che ora ti senta più sicuro nell'affrontare questi problemi scrivendo Javascript asincrono pulito e gestibile!
Se ti piace questo articolo, consulta il Blog CLA in cui discutiamo di vari argomenti su come entrare nel mondo della tecnologia. Inoltre, controlla il nostro canale YouTube per i nostri workshop gratuiti precedenti e seguici sui social media per non perdere i prossimi!