Le promesse semplificano i calcoli differiti e asincroni. Una promessa rappresenta un'operazione che non è ancora stata completata.
Sviluppatori, preparatevi a un momento cruciale nella storia dello sviluppo web.
[Drumroll begins]
Le promesse sono arrivate in JavaScript.
[Esplosione di fuochi d'artificio, pioggia di coriandoli dall'alto, la folla impazzisce]
A questo punto rientri in una di queste categorie:
- Le persone intorno a te fanno il tifo, ma non capisci tutto questo entusiasmo. Forse non sai nemmeno cosa sia una "promessa". Alzeresti le spalle, ma il peso della carta luccicante ti grava sulle spalle. In caso affermativo, non preoccuparti, ci ho messo un'eternità a capire perché mi interessano queste cose. Ti consigliamo di iniziare dall'inizio.
- Dai un pugno nell'aria. Era ora, no? Hai già utilizzato queste promesse ma ti infastidisce il fatto che tutte le implementazioni abbiano un'API leggermente diversa. Qual è l'API per la versione JavaScript ufficiale? Probabilmente vorrai iniziare dalla terminologia.
- Lo sapevi già e ti prendi gioco di chi salta su e giù come se fosse una novità. Prenditi un momento per crogiolarti nella tua superiorità, poi vai direttamente al riferimento API.
Supporto del browser e polyfill
Per portare i browser che non dispongono di un'implementazione completa delle promesse in linea con la conformità alle specifiche o per aggiungere le promesse ad altri browser e Node.js, consulta il polyfill (2 KB compressi).
Perché tutto questo trambusto?
JavaScript è single-threaded, il che significa che due script non possono essere eseguiti contemporaneamente, ma devono essere eseguiti uno dopo l'altro. Nei browser, JavaScript condivide un thread con una serie di altre cose che variano da browser a browser. Tuttavia, in genere JavaScript si trova nella stessa coda di disegno, aggiornamento degli stili e gestione delle azioni dell'utente (come l'evidenziazione del testo e l'interazione con i controlli dei moduli). L'attività in uno di questi elementi ritarda gli altri.
In quanto essere umano, sei multithread. Puoi scrivere con più dita, puoi guidare e tenere una conversazione contemporaneamente. L'unica funzione di blocco che dobbiamo gestire è lo starnuto, durante il quale tutte le attività in corso devono essere sospese per la durata dello starnuto. È piuttosto fastidioso, soprattutto quando stai guidando e cercando di sostenere una conversazione. Non vuoi scrivere codice che faccia starnutire.
Probabilmente hai utilizzato eventi e callback per risolvere il problema. Ecco gli eventi:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
Non è affatto starnutoso. Otteniamo l'immagine, aggiungiamo un paio di listener, quindi JavaScript può interrompere l'esecuzione finché non viene chiamato uno di questi listener.
Purtroppo, nell'esempio precedente è possibile che gli eventi si siano verificati prima che iniziassimo ad ascoltarli, quindi dobbiamo aggirare il problema utilizzando la proprietà "complete" delle immagini:
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
In questo modo non vengono rilevate le immagini che hanno generato errori prima che avessimo la possibilità di ascoltarle. Purtroppo, il DOM non ci offre un modo per farlo. Inoltre, questa carica un'immagine. Le cose si fanno ancora più complesse se vogliamo sapere quando è stato caricato un insieme di immagini.
Gli eventi non sono sempre il modo migliore
Gli eventi sono ideali per le azioni che possono verificarsi più volte sullo stesso oggetto, ad esempio keyup
, touchstart
e così via. Con questi eventi non ti interessa molto cosa è successo prima di collegare il listener. Ma quando si tratta di
operazioni asincrone riuscite/non riuscite, l'ideale è qualcosa di simile:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
Questo è ciò che fanno le promesse, ma con una denominazione migliore. Se gli elementi immagine HTML avessero un metodo "ready" che restituisce una promessa, potremmo fare quanto segue:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
Nella loro forma più semplice, le promesse sono un po' come i listener di eventi, tranne che:
- Una promessa può avere esito positivo o negativo una sola volta. Non può riuscire o non riuscire due volte, né può passare da riuscita a non riuscita o viceversa.
- Se una promessa è riuscita o non è riuscita e in un secondo momento aggiungi un callback di successo/errore, verrà chiamato il callback corretto, anche se l'evento si è verificato in precedenza.
Questa funzionalità è estremamente utile per la riuscita/il mancato completamento asincrono, perché ti interessa meno l'ora esatta in cui qualcosa è diventato disponibile e più reagire al risultato.
Terminologia relativa alle promesse
Domenic Denicola ha riletto la prima bozza di questo articolo e mi ha dato "F" per la terminologia. Mi ha messo in punizione, mi ha costretto a copiare States and Fates 100 volte e ha scritto una lettera preoccupata ai miei genitori. Nonostante ciò, faccio ancora molta confusione con la terminologia, ma ecco le nozioni di base:
Una promessa può essere:
- fulfilled: l'azione relativa alla promessa è riuscita
- rejected: l'azione relativa alla promessa non è riuscita
- In attesa: non è ancora stata soddisfatta o rifiutata
- settled: l'ordine è stato evaso o rifiutato
La specifica
utilizza anche il termine thenable per descrivere un oggetto simile a una promessa,
in quanto ha un metodo then
. Questo termine mi ricorda l'ex allenatore della nazionale inglese di calcio Terry Venables, quindi lo utilizzerò il meno possibile.
Le promesse arrivano in JavaScript.
Le promesse esistono da tempo sotto forma di librerie, ad esempio:
Le promesse di cui sopra e JavaScript condividono un comportamento comune e standardizzato chiamato Promises/A+. Se utilizzi jQuery, esiste qualcosa di simile chiamato Deferreds. Tuttavia, Deferred non è conforme a Promise/A+, il che lo rende leggermente diverso e meno utile, quindi fai attenzione. Anche jQuery ha un tipo Promise, ma si tratta solo di un sottoinsieme di Deferred e presenta gli stessi problemi.
Sebbene le implementazioni delle promesse seguano un comportamento standardizzato, le API complessive differiscono. Le promesse JavaScript sono simili nell'API a RSVP.js. Ecco come creare una promessa:
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
Il costruttore di promesse accetta un argomento, un callback con due parametri, resolve e reject. Esegui un'operazione all'interno del callback, magari in modo asincrono, quindi chiama resolve se tutto ha funzionato, altrimenti chiama reject.
Come throw
nel vecchio JavaScript, è consuetudine, ma non obbligatorio, rifiutare con un oggetto Error. Il vantaggio degli oggetti Error è che acquisiscono
una traccia dello stack, rendendo più utili gli strumenti di debug.
Ecco come utilizzare questa promessa:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
accetta due argomenti: un callback per un caso di successo e un altro
per il caso di errore. Entrambi sono facoltativi, quindi puoi aggiungere un callback solo per il caso di esito positivo o negativo.
Le promesse JavaScript sono nate nel DOM come "Futures", sono state rinominate "Promises" e infine sono state spostate in JavaScript. La loro presenza in JavaScript anziché nel DOM è un vantaggio perché saranno disponibili in contesti JS non browser come Node.js (se li utilizzano nelle loro API principali è un'altra questione).
Sebbene siano una funzionalità JavaScript, il DOM non ha paura di utilizzarli. Infatti, tutte le nuove API DOM con metodi asincroni di successo/errore utilizzeranno le promesse. Ciò sta già accadendo con Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams e altro ancora.
Compatibilità con altre librerie
L'API JavaScript Promises tratterà qualsiasi elemento con un metodo then()
come
simile a una promessa (o thenable
nel linguaggio delle promesse sigh), quindi se utilizzi una libreria
che restituisce una promessa Q, va bene, funzionerà bene con le nuove
promesse JavaScript.
Anche se, come ho detto, i Deferred di jQuery sono un po'… inutili. Fortunatamente puoi convertirle in promesse standard, il che è consigliabile fare il prima possibile:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
In questo caso, $.ajax
di jQuery restituisce un Deferred. Poiché ha un metodo then()
,
Promise.resolve()
può trasformarlo in una promessa JavaScript. Tuttavia,
a volte i deferred passano più argomenti ai callback, ad esempio:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
Mentre le promesse JS ignorano tutte le chiamate tranne la prima:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
Per fortuna, di solito è quello che vuoi o almeno ti dà accesso a ciò che vuoi. Inoltre, tieni presente che jQuery non segue la convenzione di passare gli oggetti Error nei rifiuti.
Codice asincrono complesso semplificato
Ok, codifichiamo alcune cose. Supponiamo di voler:
- Avvia una rotellina per indicare il caricamento
- Recupera un file JSON per una storia, che ci fornisce il titolo e gli URL di ogni capitolo
- Aggiungere un titolo alla pagina
- Recuperare ogni capitolo
- Aggiungere la storia alla pagina
- Interrompi lo spinner
… ma anche comunicare all'utente se si è verificato un problema. A questo punto, vogliamo interrompere anche lo spinner, altrimenti continuerà a girare, si stancherà e si schianterà contro un'altra UI.
Ovviamente, non utilizzeresti JavaScript per pubblicare una storia, la pubblicazione in formato HTML è più veloce, ma questo pattern è piuttosto comune quando si ha a che fare con le API: recupero di più dati, quindi esecuzione di un'azione al termine dell'operazione.
Per iniziare, occupiamoci del recupero dei dati dalla rete:
Promisifying XMLHttpRequest
Le API precedenti verranno aggiornate per utilizzare le promesse, se possibile in modo retrocompatibile. XMLHttpRequest
è un ottimo candidato, ma nel frattempo
scriviamo una semplice funzione per effettuare una richiesta GET:
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
Ora utilizziamolo:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
Ora possiamo effettuare richieste HTTP senza digitare manualmente XMLHttpRequest
, il che è fantastico, perché
meno devo vedere l'esasperante camel case di XMLHttpRequest
, più felice sarà la mia vita.
Chaining
then()
non è la fine della storia, puoi concatenare più then
per
trasformare i valori o eseguire altre azioni asincrone una dopo l'altra.
Trasformare i valori
Puoi trasformare i valori semplicemente restituendo il nuovo valore:
var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;
}).then(function(val) {
console.log(val); // 3
})
Come esempio pratico, torniamo a:
get('story.json').then(function(response) {
console.log("Success!", response);
})
La risposta è JSON, ma al momento la riceviamo come testo normale. Potremmo
modificare la nostra funzione get per utilizzare il JSON
responseType
,
ma potremmo anche risolvere il problema nel mondo delle promesse:
get('story.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
Poiché JSON.parse()
accetta un singolo argomento e restituisce un valore trasformato,
possiamo creare una scorciatoia:
get('story.json').then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
Infatti, potremmo creare una funzione getJSON()
molto facilmente:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON()
restituisce comunque una promessa, che recupera un URL e poi analizza
la risposta come JSON.
Mettere in coda le azioni asincrone
Puoi anche concatenare then
per eseguire azioni asincrone in sequenza.
Quando restituisci qualcosa da un callback then()
, è un po' magico.
Se restituisci un valore, viene chiamato il successivo then()
con quel valore. Tuttavia,
se restituisci qualcosa di simile a una promessa, la successiva then()
attende che venga risolta e viene chiamata solo quando la promessa viene risolta (esito positivo/negativo). Ad esempio:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
Qui inviamo una richiesta asincrona a story.json
, che ci fornisce un insieme di
URL da richiedere, quindi richiediamo il primo. È qui che le promesse
iniziano davvero a distinguersi dai semplici pattern di callback.
Potresti anche creare un metodo di scorciatoia per ottenere i capitoli:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
Non scarichiamo story.json
finché non viene chiamato getChapter
, ma la volta o le volte successive in cui viene chiamato getChapter
riutilizziamo la promessa della storia, quindi story.json
viene recuperato una sola volta. Evviva le promesse!
Gestione degli errori
Come abbiamo visto in precedenza, then()
accetta due argomenti, uno per l'esito positivo e uno per l'esito negativo (o per l'adempimento e il rifiuto, nel linguaggio delle promesse):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
Puoi anche utilizzare catch()
:
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
Non c'è niente di speciale in catch()
, è solo zucchero per
then(undefined, func)
, ma è più leggibile. Tieni presente che i due esempi di codice riportati sopra non si comportano allo stesso modo. Il secondo è equivalente a:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
La differenza è sottile, ma estremamente utile. I rifiuti delle promesse vanno avanti
al successivo then()
con un callback di rifiuto (o catch()
, poiché
è equivalente). Con then(func1, func2)
, verrà chiamato func1
o func2
, mai entrambi. Ma con then(func1).catch(func2)
, entrambi verranno
chiamati se func1
rifiuta, in quanto sono passaggi separati nella catena. Prendi
quanto segue:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
Il flusso riportato sopra è molto simile al normale try/catch di JavaScript: gli errori che
si verificano all'interno di un "try" vengono immediatamente inviati al blocco catch()
. Ecco la
procedura descritta sopra sotto forma di diagramma di flusso (perché adoro i diagrammi di flusso):
Segui le linee blu per le promesse che vengono soddisfatte o quelle rosse per quelle che vengono rifiutate.
Eccezioni e promesse JavaScript
I rifiuti si verificano quando una promessa viene rifiutata esplicitamente, ma anche implicitamente se viene generato un errore nel callback del costruttore:
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
Ciò significa che è utile eseguire tutto il lavoro relativo alle promesse all'interno del callback del costruttore di promesse, in modo che gli errori vengano rilevati automaticamente e diventino rifiuti.
Lo stesso vale per gli errori generati nei callback then()
.
get('/').then(JSON.parse).then(function() {
// This never happens, '/' is an HTML page, not JSON
// so JSON.parse throws
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
Gestione degli errori in pratica
Con la nostra storia e i nostri capitoli, possiamo utilizzare catch per mostrare un errore all'utente:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.html);
}).catch(function() {
addTextToPage("Failed to show chapter");
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Se il recupero di story.chapterUrls[0]
non riesce (ad es. http 500 o l'utente è offline),
vengono ignorati tutti i callback di successo successivi, incluso quello in
getJSON()
che tenta di analizzare la risposta come JSON, e viene ignorato anche il
callback che aggiunge chapter1.html alla pagina. Invece, passa al callback catch. Di conseguenza, alla pagina verrà aggiunto il messaggio "Impossibile mostrare il capitolo" se
una delle azioni precedenti non è andata a buon fine.
Come try/catch di JavaScript, l'errore viene rilevato e il codice successivo continua, quindi lo spinner è sempre nascosto, come desiderato. Il codice precedente diventa una versione asincrona non bloccante di:
try {
var story = getJSONSync('story.json');
var chapter1 = getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}
catch (e) {
addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'
Potresti voler catch()
semplicemente per scopi di logging, senza eseguire il ripristino
dall'errore. Per farlo, rilancia l'errore. Potremmo farlo nel nostro metodo getJSON()
:
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
Siamo riusciti a recuperare un capitolo, ma vogliamo tutti i capitoli. Facciamo in modo che accada.
Parallelismo e sequenziamento: ottenere il meglio da entrambi
Pensare in modo asincrono non è facile. Se hai difficoltà a iniziare, prova a scrivere il codice come se fosse sincrono. In questo caso:
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
Funziona. ma si sincronizza e blocca il browser durante il download. Per
far funzionare questa operazione in modo asincrono, utilizziamo then()
per fare in modo che le cose avvengano una dopo l'altra.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
Ma come possiamo scorrere gli URL dei capitoli e recuperarli in ordine? Questo non funziona:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach
non è compatibile con l'asincronia, quindi i nostri capitoli vengono visualizzati nell'ordine in cui vengono scaricati, ovvero come è stato scritto Pulp Fiction. Non siamo
in Pulp Fiction, quindi risolviamo il problema.
Creazione di una sequenza
Vogliamo trasformare il nostro array chapterUrls
in una sequenza di promesse. Possiamo farlo utilizzando then()
:
// Start off with a promise that always resolves
var sequence = Promise.resolve();
// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
// Add these actions to the end of the sequence
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
})
È la prima volta che vediamo Promise.resolve()
, che crea una
promessa che si risolve nel valore che le assegni. Se gli passi un'istanza di Promise
, la restituirà semplicemente (nota:si tratta di una modifica alla specifica che alcune implementazioni non seguono ancora). Se
gli passi qualcosa di simile a una promessa (ha un metodo then()
), crea un
Promise
autentico che viene soddisfatto/rifiutato nello stesso modo. Se passi
un altro valore, ad esempio Promise.resolve('Hello')
, crea una
promessa che viene soddisfatta con quel valore. Se lo chiami senza alcun valore,
come sopra, viene compilato con "undefined".
Esiste anche Promise.reject(val)
, che crea una promessa che viene rifiutata con
il valore che le viene assegnato (o undefined).
Possiamo riordinare il codice precedente utilizzando
array.reduce
:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Add these actions to the end of the sequence
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
In questo modo si esegue la stessa operazione dell'esempio precedente, ma non è necessaria la variabile "sequenza" separata. La nostra funzione di callback reduce viene chiamata per ogni elemento dell'array.
"sequence" è Promise.resolve()
la prima volta, ma per il resto delle
chiamate "sequence" è ciò che abbiamo restituito dalla chiamata precedente. array.reduce
è molto utile per ridurre un array a un singolo valore, che in questo caso
è una promessa.
Mettiamo insieme tutti i pezzi:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
Ed ecco una versione completamente asincrona della versione di sincronizzazione. Ma possiamo fare di meglio. Al momento la nostra pagina viene scaricata in questo modo:
I browser sono piuttosto bravi a scaricare più elementi contemporaneamente, quindi perdiamo prestazioni scaricando i capitoli uno dopo l'altro. Quello che vogliamo fare è scaricarli tutti contemporaneamente, quindi elaborarli quando sono arrivati. Fortunatamente esiste un'API per questo:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
accetta un array di promesse e ne crea una che viene soddisfatta
quando tutte vengono completate correttamente. Ottieni un array di risultati (qualunque
sia la promessa soddisfatta) nello stesso ordine delle promesse che hai passato.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
A seconda della connessione, questo può essere più veloce di secondi rispetto al caricamento uno alla volta ed è meno codice rispetto al nostro primo tentativo. I capitoli possono essere scaricati in qualsiasi ordine, ma vengono visualizzati sullo schermo nell'ordine corretto.
Tuttavia, possiamo comunque migliorare le prestazioni percepite. Quando arriverà il primo capitolo, dovremmo aggiungerlo alla pagina. In questo modo, l'utente può iniziare a leggere prima che arrivino gli altri capitoli. Quando arriva il capitolo 3, non lo aggiungiamo alla pagina perché l'utente potrebbe non rendersi conto che manca il capitolo 2. Quando arriverà il capitolo 2, potremo aggiungere i capitoli 2 e 3 e così via.
Per farlo, recuperiamo contemporaneamente il JSON di tutti i capitoli, quindi creiamo una sequenza per aggiungerli al documento:
getJSON('story.json')
.then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download in parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence
.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Ed ecco fatto, il meglio di entrambi! Per la distribuzione di tutti i contenuti è necessario lo stesso tempo, ma l'utente riceve il primo bit di contenuti prima.
In questo esempio banale, tutti i capitoli arrivano più o meno nello stesso momento, ma il vantaggio di visualizzarne uno alla volta sarà più evidente con capitoli più numerosi e più grandi.
Eseguire le operazioni precedenti con callback o eventi in stile Node.js richiede circa il doppio del codice, ma soprattutto non è altrettanto facile da seguire. Tuttavia, questa non è la fine della storia per le promesse, che diventano ancora più semplici se combinate con altre funzionalità ES6.
Gara bonus: funzionalità ampliate
Da quando ho scritto questo articolo, la possibilità di utilizzare le promesse è aumentata notevolmente. A partire da Chrome 55, le funzioni asincrone hanno consentito di scrivere codice basato su promesse come se fosse sincrono, ma senza bloccare il thread principale. Puoi scoprire di più in questo articolo sulle funzioni asincrone. Esiste un supporto diffuso sia per le promesse che per le funzioni asincrone nei principali browser. Puoi trovare i dettagli nei riferimenti a Promise e alla funzione async di MDN.
Un ringraziamento speciale ad Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans e Yutaka Hirano che hanno riletto questo articolo e apportato correzioni/consigli.
Inoltre, grazie a Mathias Bynens per l'aggiornamento di varie parti dell'articolo.