Articolo originale: https://www.freecodecamp.org/news/synchronous-vs-asynchronous-in-javascript/

Voglio iniziare questo articolo con la domanda "Cos'è JavaScript?". Beh, ecco la risposta più disorientante che ho trovato finora:

JavaScript è un linguaggio di programmazione mono-thread, non bloccante, asincrono e concorrente che possiede molta flessibilità.

Aspetta un attimo – dice mono-thread e asincrono allo stesso tempo? Sapendo cosa vuol dire mono-thread, viene spontaneo associare il concetto alle operazioni sincrone. Ma allora come può JavaScript essere asincrono?

In questo articolo, impareremo tutto sulle parti sincrone e asincrone di JavaScript. Vengono utilizzate nella programmazione web praticamente ogni giorno.

Se desideri guardare un video, questo articolo è disponibile anche in versione video tutorial (risorsa in inglese) qui: 🙂

In questo articolo, imparerai:

  • In che modo JavaScript è sincrono
  • Come avvengono le operazioni asincrone anche se JavaScript è mono-thread.
  • In che modo comprendere i processi sincroni vs asincroni ti aiuta a capire le Promise in JavaScript.
  • Molti esempi semplici ma importanti per trattare questi concetti nel dettaglio.

Le funzioni in JavaScript sono cittadini di prima categoria

In JavaScript, è possibile creare e modificare una funzione, usarla come argomento, restituirla da un'altra funzione e assegnarla a una variabile. Tutte queste abilità consentono di usare le funzioni per posizionare del codice in modo logico ovunque.

block-function
Righe di codice organizzato logicamente in funzioni

Per comunicare al motore JavaScript di eseguire delle funzioni, dobbiamo invocarle, in questo modo:

// Definizione di funzione
function f1() {
    // Fa qualcosa
    // Fa qualcos'altro
    // Altro ancora
    // E così via...
}

// Invocazione di funzione
f1();

Ogni riga in una funzione viene eseguita in modo predefinito sequenzialmente, una alla volta. Lo stesso è applicabile anche anche all'invocazione di più funzioni. Ancora una volta, riga per riga.

JavaScript sincrono – Come funziona il function execution stack

Quindi cosa accade quando definiamo una funzione e la invochiamo? Il motore JavaScript gestisce una struttura di dati stack chiamata function execution stack. Lo scopo dello stack è di tenere traccia della funzione attualmente in esecuzione, nel seguente modo:

  • Quando il motore JavaScript invoca una funzione, la aggiunge allo stack e ne avvia l'esecuzione.
  • Se la funzione attualmente eseguita chiama un'altra funzione, il motore aggiunge la seconda funzione allo stack e ne avvia l'esecuzione.
  • Una volta terminata l'esecuzione della seconda funzione, il motore la rimuove dallo stack.
  • Si torna al controllo dell'esecuzione della prima funzione dal punto in cui è rimasta l'ultima volta.
  • Una volta che la prima funzione è terminata, il motore la rimuove dallo stack.
  • Il tutto prosegue finché non c'è più nulla da inserire nello stack.

Il function execution stack è anche detto call stack.

stack
Function Execution Stack

Diamo un'occhiata a un esempio di tre funzioni che vengono eseguite una alla volta:

function f1() {
  // del codice
}
function f2() {
  // del codice
}
function f3() {
  // del codice
}

// Invocazione delle funzioni, una alla volta
f1();
f2();
f3();

Ora vediamo cosa accade con il function execution stack:

first-flow
L'ordine di esecuzione mostrato passo passo

Hai visto cosa accade qui? Per prima cosa, f1() va nello stack, viene eseguita e poi rimossa. Poi lo stesso accade a f2() e infine a f3(). Dopodiché lo stack è vuoto, non c'è più nulla da eseguire.

Ok, adesso passiamo a un esempio più complesso. Qui abbiamo una funzione f3() che invoca un'altra funzione f2() che, a sua volta, invoca un'altra funzione f1().

function f1() {
  // Del codice
}
function f2() {
  f1();
}
function f3() {
  f2();
}
f3();

Vediamo cosa accade nel function execution stack:

second-flow
L'ordine di esecuzione mostrato passo passo

Inizialmente f3() va nello stack, invocando un'altra funzione, f2(). Adesso viene inserita f2() mentre f3() resta nello stack. La funzione f2() invoca f1(). Per f1() è il momento di andare nello stack, insieme a f2() e f3(), che restano al suo interno.

L'esecuzione di f1() termina e viene rimossa dallo stack. Subito dopo, f2() termina e infine f3().

La conclusione è che tutto ciò che accade all'interno del function execution stack è sequenziale. Questa è la parte sincrona di JavaScript. Il thread principale di JavaScript si assicura di gestire tutto quello che è nello stack prima di passare ad altro.

Ottimo! Adesso che sappiamo come funzionano le operazioni sincrone in JavaScript, diamo un'occhiata al lato asincrono. Sei pronto?

JavaScript asincrono – Come funzionano API browser e Promise

Il termine asincrono vuol dire che non accade allo stesso tempo. Cosa significa nel contesto di JavaScript?

Generalmente, eseguire le cose in sequenza funziona bene. Ma a volte può essere necessario recuperare dei dati da un server o eseguire una funzione in ritardo, e dunque eseguire del codice in modo asincrono.

In queste circostanze, potresti non desiderare che il motore JavaScript blocchi l'esecuzione sequenziale di altro codice. Quindi il motore JavaScript deve gestire le cose in modo un po' più efficiente in questo caso.

Possiamo classificare la maggior parte delle operazioni asincrone in JavaScript secondo due impulsi primari:

  1. Eventi o funzioni di API browser/API Web. Questi includono metodi come setTimeout o gestori come click, mouse over, scroll e altri.
  2. Promise. Un oggetto JavaScript unico che consente di svolgere operazioni asincrone.

Non preoccuparti se non conosci le promise. Non ti serve sapere altro per seguire questo articolo. Alla fine di questo articolo troverai dei link in modo da poter iniziare a studiare le promise nel modo più adatto a un principiante.

Come gestire API browser / API web

Le API browser come setTimeout e i gestori di eventi si basano sulle funzioni callback. Una funzione callback viene eseguita quando si completa un'operazione asincrona. Ecco un esempio di come funziona setTimeout:

function printMe() {
  console.log('print me');
}

setTimeout(printMe, 2000);

La funzione setTimeout esegue una funzione dopo che è trascorso un certo tempo. Nel codice qui sopra, il testo print me viene stampato sulla console dopo un intervallo di 2 secondi.

Adesso assumiamo di avere alcune righe di codice dopo la funzione setTimeout:

function printMe() {
  console.log('print me');
}

function test() {
  console.log('test');
}

setTimeout(printMe, 2000);
test();

Cosa ci aspettiamo che accada qui? Quale pensi che sarà l'output?

Il motore JavaScript aspetterà 2 secondi per passare all'invocazione della funzione test() generando questo output?

printMe
test

O terrà da parte la funzione callback di setTimeout continuando l'ordine di esecuzione? Quindi l'output potrebbe essere questo forse:

test
printMe

Se pensi che risposta corretta sia l'ultima, hai indovinato. È qui che entra in gioco il meccanismo asincrono.

Come funziona la callback queue (task queue) in JavaScript

JavaScript mantiene una code di funzioni callback, chiamata callback queue o task queue. Una struttura di dati di tipo coda è First-In-First-Out (FIFO). La funzione callback che entra per prima nella coda ha l'opportunità di uscirne per prima. Ma ecco delle domande importanti:

  • Quand'è che il motore JavaScript la mette in coda?
  • Quando la rimuove dalla coda?
  • Dove va a finire quando esce dalla coda?
  • E più importante, in che modo tutte queste parti sono collegate alla parte asincrona di JavaScript?

Wow, un sacco di domande! Cerchiamo di rispondere con l'aiuto della seguente immagine:

taskQ

L'immagine qui sopra mostra il normale call stack che abbiamo già visto precedentemente. Esistono due sezioni aggiuntive per tracciare se le API browser (come setTimeout) entrano in gioco e mettere in coda le loro funzioni callback.

Il motore JavaScript continua a eseguire le funzioni nel call stack. Dato che non inserisce direttamente le funzioni callback nello stack, non c'è nessun problema con del codice in attesa o che blocca l'esecuzione nello stack.

Il motore crea un loop per valutare periodicamente la coda e trovare ciò che bisogna estrarre. Estrae una funzione callback dalla coda e la inserisce nello stack quando quest'ultimo è vuoto. Poi la funzione callback viene eseguita come qualsiasi altra funzione nello stack e il loop continua. Questo loop è ben conosciuto con il nome di event loop.

La morale della favola è che:

  • Quando entra in gioco una API browser, parcheggia le sue funzioni in una coda.
  • L'esecuzione del codice nello stack prosegue come al solito.
  • L'event loop verifica se c'è una funzione callback in coda.
  • Se c'è, la richiama allo stack e la esegue.
  • Il loop continua.

Molto bene, adesso vediamo come funziona il codice qui sotto:

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    f2();
}

main();

Il codice esegue una funzione setTimeout con una funzione callback f1(). Nota che le abbiamo assegnato un ritardo di zero. Ciò vuol dire che ci aspettiamo che la funzione f1() sia eseguita immediatamente. Proprio dopo setTimeout, eseguiamo un'altra funzione, f2().

Quale pensi che sarà l'output? Eccolo qui:

main
f2
f1

Potresti pensare che f1 debba essere stampata prima di f2, dato che non abbiamo assegnato alcun ritardo a f1. Ma in realtà no. Ricordi il meccanismo dell'event loop di cui abbiamo parlato prima? Diamo un'occhiata o ogni passaggio di questo meccanismo per il nostro codice.

third-flow
Event loop – esecuzione passo passo

Ecco i passaggi descritti:

  1. La funzione main() va all'interno del call stack.
  2. Contiene un console log per stampare la parola main. console.log('main') viene eseguito ed esce dallo stack.
  3. È il turno dell'API browser setTimeout.
  4. La funzione callback va nella coda callback.
  5. Nello stack, l'esecuzione avviene come al solito, quindi f2() va nello stack. La funzione console log di f2() viene eseguita. Entrambe escono dallo stack.
  6. Anche main() esce dallo stack.
  7. L'event loop si accorge che lo stack è vuoto e c'è una funzione callback in coda.
  8. La funzione callback f1() passa allo stack e viene eseguita. Viene eseguito il console log, e anche f1() esce dallo stack.
  9. A questo punto non c'è nient'altro da eseguire nello stack o in coda.

Spero che adesso ti sia chiaro il modo in cui funziona internamente la parte asincrona di JavaScript. Ma non è tutto. Adesso parleremo delle promise.

Come vengono gestite le promise dal motore JavaScript

In JavaScript, le promise sono degli oggetti speciali che consentono di svolgere operazioni asincrone.

Puoi creare una promise usando il costruttore Promise, a cui occorre passare una funzione executor. In questa funzione va definito ciò che vuoi che accada quando una promise ha successo o dà un errore. Puoi farlo rispettivamente con resolve e reject.

Ecco un esempio di promise in JavaScript:

const promise = new Promise((resolve, reject) =>
        resolve('Sono una promise risolta');
);

Dopo che una promise è stata eseguita, possiamo gestire il risultato usando il metodo .then() ed eventuali errori con il metodo .catch().

promise.then(result => console.log(result))

Si utilizzano le promise ogni volta che occorre richiedere dei dati usando il metodo fetch().

Qui il punto è che il motore JavaScript non utilizza la stessa coda callback che abbiamo visto precedentemente per le API browser, ma utilizza una coda apposita chiamata job queue.

Cos'è la job queue in JavaScript?

Ogni volta che c'è una promise nel codice, la funzione executor va nella job queue. L'event loop funziona come di consueto, cerca nelle code e, quando lo stack è libero, dà priorità agli elementi nella job queue rispetto agli elementi della callback queue.

Un elementi nella coda callback è detto macro task, mentre un elemento nella job queue è detto micro task.

L'intero flusso procede in questo modo:

  • Per ogni iterazione dell'event loop, viene completata un'attività della callback queue.
  • Una volta che un'attività viene completata, l'event loop visita la job queue. Completa tutte le micro task nella job queue prima di passare all'attività successiva.
  • Se entrambe le code hanno delle entrate nello stesso momento, la job queue ottiene la priorità sulla callback queue.

L'immagine di seguito mostra l'aggiunta della job queue a fianco degli altri elementi preesistenti.

JObQ

Adesso cerchiamo di capire meglio questa sequenza:

function f1() {
    console.log('f1');
}

function f2() {
    console.log('f2');
}

function main() {
    console.log('main');
    
    setTimeout(f1, 0);
    
    new Promise((resolve, reject) =>
        resolve('I am a promise')
    ).then(resolve => console.log(resolve))
    
    f2();
}

main();

Nel codice qui sopra, abbiamo una funzione setTimeout() come prima, ma abbiamo aggiunto una promise subito dopo di essa. Ricorda cosa abbiamo imparato e cerca di capire quale sarà l'output.

Se la tua risposta è questa, allora hai indovinato:

main
f2
I am a promise
f1

Ora vediamo la sequenza delle azioni:

fourth-flow
Callback queue vs. Job queue

La sequenza è quasi uguale alla precedente, ma è importante notare come gli elementi della job queue hanno la priorità su quelli nella task queue, e come non ha importanza che il ritardo della funzione setTimeout sia zero: conta sempre il fatto che la job queue viene prima della callback queue.

Ottimo, abbiamo imparato tutto quello che ci serve per capire l'esecuzione sincrona e asincrona in JavaScript.

Un quiz per te!

Mettiti alla prova con questo quiz. Cerca di capire l'output del codice seguente e applica tutto quello che hai imparato finora:

function f1() {
 console.log('f1');
}

function f2() { 
    console.log('f2');
}

function f3() { 
    console.log('f3');
}

function main() {
  console.log('main');

  setTimeout(f1, 50);
  setTimeout(f3, 30);

  new Promise((resolve, reject) =>
    resolve('Sono una Promise, proprio dopo f1 e f3! Davvero?')
  ).then(resolve => console.log(resolve));
    
  new Promise((resolve, reject) =>
    resolve('Sono una Promise dopo una Promise!')
  ).then(resolve => console.log(resolve));

  f2();
}

main();

Ecco l'output atteso:

main
f2
Sono una Promise, proprio dopo f1 e f3! Davvero?
Sono una Promise dopo una Promise!
f3
f1

Per altri quiz di questo tipo vai su questo repository ed esercitati.

Se sei bloccato o hai bisogno di chiarimenti, puoi sempre mandarmi un messaggio su Twitter.

In conclusione

Per riassumere:

  • Il motore JavaScript usa la struttura di dati stack per tenere traccia delle funzioni correntemente in esecuzione. Lo stack viene chiamato function execution stack.
  • Il function execution stack (o call stack) esegue le funzioni sequenzialmente, riga per riga, una alla volta.
  • Le API browser/web utilizzano delle funzioni callback per completare le attività che richiedono operazioni asincrone. Le funzioni callback vengono inserite nella callback queue.
  • Le funzioni executor delle promise vengono inserite nella job queue.
  • Per ogni iterazione dell'event loop, viene completata una macro task nella callback queue.
  • Una volta che un'attività viene completata, l'event loop visita la job queue e completa tutte le micro task nella job queue prima di andare oltre.
  • Se entrambe le code hanno delle entrate nello stesso momento, la job queue ottiene la priorità sulla callback queue.

Prima di salutarci...

Per ora è tutto. Spero che tu abbia trovato interessante questo articolo e che ti abbia aiutato a capire meglio i concetti alla base delle operazioni sincrone e asincrone in JavaScript.

Se vuoi, puoi seguirmi su Twitter(@tapasadhikary), su Youtube e GitHub(atapas).

Come promesso, ecco alcuni articoli (risorse in inglese) che potresti trovare utili: