📢 Nuovo Corso Laravel API disponibile!

Controllo del flusso asincrono

Controllo del flusso asincrono

Al suo nucleo, JavaScript è progettato per essere non bloccante sul “thread” principale, che è dove vengono renderizzate le viste. Puoi immaginare l’importanza di questo nel browser. Quando il thread principale diventa bloccato, si verifica la famigerata “congelazione” che gli utenti finali temono, e nessun altro evento può essere inviato, comportando la perdita di acquisizione dati, ad esempio.

Ciò crea alcune vincoli unici che solo uno stile di programmazione funzionale può curare. Questo è dove entrano in gioco i callback.

Tuttavia, i callback possono diventare difficili da gestire in procedure più complesse. Questo porta spesso a un “inferno dei callback” in cui molte funzioni nidificate con callback rendono il codice più difficile da leggere, debuggare, organizzare, ecc.

async1(function (input, result1) {
async2(function (result2) {
async3(function (result3) {
async4(function (result4) {
async5(function (output) {
// fai qualcosa con l'output
});
});
});
});
});

Naturalmente, nella vita reale ci sarebbero molto probabilmente ulteriori linee di codice per gestire result1, result2, ecc., quindi la lunghezza e la complessità di questo problema di solito si traducono in un codice che appare molto più disordinato rispetto all’esempio sopra.

Ed è qui che le funzioni entrano in gioco. Operazioni più complesse sono composte da molte funzioni:

  1. stile iniziatore / input
  2. middleware
  3. terminatore

Lo “stile iniziatore / input” è la prima funzione nella sequenza. Questa funzione accetterà l’input originale, se presente, per l’operazione. L’operazione è una serie eseguibile di funzioni, e l’input originale sarà principalmente:

  1. variabili in un ambiente globale
  2. invocazione diretta con o senza argomenti
  3. valori ottenuti da richieste di sistema di file o di rete

Le richieste di rete possono essere richieste in arrivo avviate da una rete esterna, da un’altra applicazione sulla stessa rete o dall’app stessa sulla stessa o su una rete esterna.

Una funzione middleware restituirà un’altra funzione, e una funzione terminale invocherà il callback. Di seguito viene illustrato il flusso delle richieste di rete o di sistema di file. Qui la latenza è 0 perché tutti questi valori sono disponibili in memoria.

function finale(someInput, callback) {
callback(`${someInput} e terminato eseguendo il callback `);
}
function middleware(someInput, callback) {
return finale(`${someInput} toccato dal middleware `, callback);
}
function inizializza() {
const someInput = "ciao, questa è una funzione ";
middleware(someInput, function (result) {
console.log(result);
// richiede il callback per `restituire` il risultato
});
}
inizializza();

Gestione dello stato

Le funzioni possono essere o meno dipendenti dallo stato. La dipendenza dallo stato sorge quando l’input o un’altra variabile di una funzione dipende da una funzione esterna.

In questo modo ci sono due strategie principali per la gestione dello stato:

  1. passaggio diretto di variabili a una funzione e
  2. acquisizione di un valore di variabile da una cache, sessione, file, database, rete o altra fonte esterna.

Nota, non ho menzionato la variabile globale. Gestire lo stato con variabili globali è spesso un anti-pattern disordinato che rende difficile o impossibile garantire lo stato. Le variabili globali nei programmi complessi dovrebbero essere evitate quando possibile.

Controllo del flusso

Se un oggetto è disponibile in memoria, è possibile l’iterazione e non ci sarà alcun cambio nel flusso di controllo:

function getSong() {
let _song = "";
let i = 100;
for (i; i > 0; i -= 1) {
_song += `${i} bottiglie al muro, ne prendi una giù e la passi, ${
i - 1
} bottiglie di birra al muro\n`;
if (i === 1) {
_song += "Ehi, prendiamo un po' di più di birra";
}
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("la canzone è vuota, DAMMI UNA CANZONE!");
console.log(_song);
}
const song = getSong();
// questo funzionerà
singSong(song);

Tuttavia, se i dati esistono al di fuori della memoria, l’iterazione non funzionerà più:

function getSong() {
let _song = "";
let i = 100;
for (i; i > 0; i -= 1) {
/* eslint-disable no-loop-func */
setTimeout(function () {
_song += `${i} bottiglie al muro, ne prendi una giù e la passi, ${
i - 1
} bottiglie di birra al muro\n`;
if (i === 1) {
_song += "Ehi, prendiamo un po' di più di birra";
}
}, 0);
/* eslint-enable no-loop-func */
}
return _song;
}
function singSong(_song) {
if (!_song) throw new Error("la canzone è vuota, DAMMI UNA CANZONE!");
console.log(_song);
}
const song = getSong("birra");
// questo non funzionerà
singSong(song);
// Uncaught Error: la canzone è vuota, DAMMI UNA CANZONE!

Perché è successo questo? setTimeout istruisce la CPU a memorizzare le istruzioni altrove sul bus e a pianificare il ritiro dei dati in un momento successivo. Passano migliaia di cicli della CPU prima che la funzione colpisca di nuovo al marcatore di 0 millisecondi, la CPU preleva le istruzioni dal bus e le esegue. L’unico problema è che la canzone (”) è stata restituita migliaia di cicli prima.

La stessa situazione si presenta nel trattare con i sistemi di file e le richieste di rete. Il thread principale semplicemente non può essere bloccato per un periodo di tempo indeterminato, quindi utilizziamo i callback per pianificare l’esecuzione del codice nel tempo in modo controllato.

Sarai in grado di eseguire quasi tutte le tue operazioni con i seguenti 3 pattern:

  1. In serie: le funzioni verranno eseguite in un rigoroso ordine sequenziale, questo è più simile ai loop for.
// operazioni definite altrove e pronte per essere eseguite
const operations = [
{ func: function1, args: args1 },
{ func: function2, args: args2 },
{ func: function3, args: args3 },
];
function executeFunctionWithArgs(operation, callback) {
// esegue la funzione
const { args, func } = operation;
func(args, callback);
}
function proceduraSeriale(operation) {
if (!operation) process.exit(0); // finito
executeFunctionWithArgs(operation, function (result) {
// continua DOPO il callback
proceduraSeriale(operations.shift());
});
}
proceduraSeriale(operations.shift());
  1. Completamente parallelo: quando l’ordine non è un problema, come inviare e-mail a una lista di 1.000.000 di destinatari.
let count = 0;
let success = 0;
const failed = [];
const recipients = [
{ name: "Bart", email: "bart@tld" },
{ name: "Marge", email: "marge@tld" },
{ name: "Homer", email: "homer@tld" },
{ name: "Lisa", email: "lisa@tld" },
{ name: "Maggie", email: "maggie@tld" },
];
function dispatch(recipient, callback) {
// `sendEmail` è un client SMTP ipotetico
sendMail(
{
subject: "Cena stasera",
message: "Abbiamo un sacco di cavoli nel piatto. Vieni?",
smtp: recipient.email,
},
callback
);
}
function finale(result) {
console.log(`Risultato: ${result.count} tentativi \
e ${result.success} e-mail riuscite`);
if (result.failed.length)
console.log(`Impossibile inviare a: \
\n${result.failed.join("\n")}\n`);
}
recipients.forEach(function (recipient) {
dispatch(recipient, function (err) {
if (!err) {
success += 1;
} else {
failed.push(recipient.name);
}
count += 1;
if (count === recipients.length) {
finale({
count,
success,
failed,
});
}
});
});
  1. Parallelo limitato: parallelo con limite, come inviare con successo 1.000.000 di destinatari da una lista di 10^7 utenti.
let successCount = 0;
function finale() {
console.log(`spedite ${successCount} e-mail`);
console.log("finito");
}
function dispatch(recipient, callback) {
// `sendEmail` è un client SMTP ipotetico
sendMail(
{
subject: "Cena stasera",
message: "Abbiamo un sacco di cavoli nel piatto. Vieni?",
smtp: recipient.email,
},
callback
);
}
function inviaUnMilioneDiEmail() {
getListaDiDieciMilioniDiGrandiEmail(function (err, bigList) {
if (err) throw err;
function seriale(recipient) {
if (!recipient || successCount >= 1000000) return finale();
dispatch(recipient, function (_err) {
if (!_err) successCount += 1;
seriale(bigList.pop());
});
}
seriale(bigList.pop());
});
}
inviaUnMilioneDiEmail();

Ognuno ha i suoi casi d’uso, vantaggi e problemi che puoi sperimentare e approfondire ulteriormente. Ricorda soprattutto di modularizzare le tue operazioni e utilizzare i callback! Se hai dubbi, tratta tutto come se fosse middleware!