Sommario
< Home
Stampa

Javascript asincrono

Event loop

La grande forza delle applicazioni Javascript è che possono aggiornare la pagina web modificando dinamicamente l’HTML. E’ quindi possibile elaborare dati dell’utente, aggiornare la pagina e renderla in tutto e per tutto simile a quella di una vera applicazione desktop. Ma c’è di più: con Javascript è possibile anche interagire con Internet, scambiando quindi dati con servizi remoti nel Web.

Per capire come funziona dobbiamo però introdurre due nuovi concetti:

1) Cos’è e come funziona Javascript asincrono tramite il meccanismo noto come “event loop”

2) Come funzionano le richieste di dati su Internet, col protocollo HTTP.

Il primo argomento sarà trattato in questa sede, il secondo nella prossima lezione.

Partiamo da queste funzioni (parte del BOM ovvero l’oggetto window):

FunzionalitàEsempio
setTimeout(funzione, x);setTimeout(() => {    
console.log("ciao"); 
}, 2000);
setInterval(funzione, x);const i = setInterval(() => {    
console.log("ciao"); 
}, 2000);

Queste funzioni ricevono come parametro una funzione e ne schedulano l’esecuzione nel futuro (una volta o periodicamente). Questo tuttavia porta ad un comportamento non immediatamente visibile quando eseguiamo questo tipo di codice.

Vediamo un esempio:

const f = (() => {

   console.log("1");

};

setTimeout(f, 100);

console.log("2");

Quello che succede è che viene prima stampato 2 e poi viene stampato 1.

Questo perchè lo script JS con setTimeout schedula l’esecuzione della funzione f in un tempo futuro, senza fermare l’esecuzione della funzione corrente.

Quindi quello che succede è che lo script (o la funzione) corrente viene eseguito fino al suo completamento cioè fino all’ultima istruzione. Poi si ferma tutto e Javascript tornerà solo dopo 100ms, cioè quando sarà schedulata la funzione f.

Ora ipotizziamo che dopo un setTimeout continuiamo ad impegnare Javascript con una elaborazione molto lunga ad esempio:

const f = (() => {
  console.log("1");
};
setTimeout(f, 100);
for (let i=0; i<100000000; i++) { 
  console.log(i); 
}

E’ probabile che il ciclo for ci metta ben di più di 100 millisecondi, e quindi sicuramente non riesce a terminare prima che parti l’esecuzione della funzione f. Quindi cosa succede? Che Javascript comunque termina lo script/funzione corrente, e soltanto dopo vada a vedere se c’è una funzione schedulata per il futuro.

Quindi setTimeout non garantisce che venga eseguita esattamente dopo 100ms, ma solo che venga eseguita non prima di 100 ms. Questo perchè in JS:

  • viene eseguita una sola funzione alla volta;
  • esiste una coda di funzioni che verrà eseguita (una funzione alla volta) al termine della esecuzione della funzione corrente.

Questo vuol dire che se eseguito N setTimeout con tempi anche variabili, JS aspetterà i tempi indicati e al termine metterà in coda le funzioni da eseguire dopo il timeout.

In pratica quel che succede è il runtime di JS non fa altro che eseguire una per volta delle funzioni inserite in una coda di esecuzione. Ogni volta che il runtime ha finito di eseguire una funzione, controlla nella coda se è presente una nuova funzione da eseguire. Questo algoritmo, che possiamo immaginare come una specie di ciclo while infinito, viene chiamato “event loop” ed è alla base del funzionamento di Javascript. [1].

Ogni volta che eseguiamo setTimeout (o setInterval), il runtime attiva un thread in background che riceve l’istruzione, attende il tempo indicato, ed aggiunge alla coda di esecuzione la funzione richiesta. 

Ogni volta che il runtime finisce una funzione che sta eseguendo controlla (nel ciclo “event loop”) se c’è qualcosa in coda e lo esegue.

Questo comportamento non vale solo per le funzioni qui sopra. Vale per esempio per la pressione di un pulsante (onclick). Anche questo caso è come se venisse creato un setTimeout (con tempo 0), il runtime (che agisce su pià thread separati, è una applicazione C++) ascolta gli eventi utente, controlla se ci sono funzioni associate, e se presenti le mette in coda di esecuzione. Ma Javascript non esegue nulla finché è in esecuzione un’altra funzione. Vedremo che sono molti altri gli eventi che possono accadere, non solo generati dall’utente, che generano l’inserimento in coda di una funzione. Anche il programmatore può generare eventi, come abbiamo visto con i setTimeout e setInterval, ma anche con oggetti ad hoc, come vedremo.

L’algoritmo di funzionamento di Javascript altri non è che un semplice schema produttore-consumatore, dove sono presenti uno o più produttori (i thread secondari che aggiungono funzioni alla coda di esecuzione in seguito a qualche tipo di eventi) ed un solo consumatore, l’event lopp, che in modo ciclico svuota la coda di esecuzione. Javascript non garantisce mai quando una funzione in coda sarà eseguita, ma garantisce solo che chi è messo in coda e prima o poi sarà eseguito. Ma se è in corso un’altra operazione, le funzioni in coda devono aspettare. 

Per questa ragione è sempre bene scrivere funzioni brevi ed evitare cicli di grande complessità, per evitare la formazione di lunghe code di esecuzione.


[1] (si può verificare che effettivamente nel codice sorgente C++ di V8 è presente proprio un ciclo while).