Le promise semplificano i calcoli differiti e asincroni. Una promessa rappresenta un'operazione non ancora completata.
Sviluppatori, preparatevi a un momento cruciale nella storia dello sviluppo web.
[Inizia il rullo di tamburi]
Le promise sono arrivate in JavaScript.
[Fireworks explode, glittery paper rains from above, the crowd goes wild]
A questo punto, rientri in una di queste categorie:
- Le persone intorno a te esultano, ma non capisci il motivo di tanto entusiasmo. Forse non sai nemmeno cosa sia una "promessa". Vorresti fare spallucce, ma il peso della carta glitterata ti grava sulle spalle. Se è così, non preoccuparti, mi ci è voluto molto tempo per capire perché dovrei interessarmi a queste cose. Probabilmente vorrai iniziare dall'inizio.
- Tiri un pugno nell'aria! Era ora, no? Hai già utilizzato queste Promise, ma ti dà fastidio che tutte le implementazioni abbiano un'API leggermente diversa. Qual è l'API per la versione JavaScript ufficiale? Probabilmente vorrai iniziare con la terminologia.
- Lo sapevi già e ti prendi gioco di chi si entusiasma 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 rendere i browser privi di un'implementazione completa delle promise conformi alle specifiche o per aggiungere promise ad altri browser e a Node.js, consulta il polyfill (2k gzipped).
Perché tutto questo trambusto?
JavaScript è a thread singolo, il che significa che due parti di script non possono essere eseguite contemporaneamente, ma devono essere eseguite una dopo l'altra. Nei browser, JavaScript condivide un thread con un sacco di altre cose che variano da browser a browser. In genere, però, JavaScript si trova nella stessa coda di pittura, 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 digitare con più dita, puoi guidare e conversare contemporaneamente. L'unica funzione di blocco che dobbiamo gestire è lo starnuto, in cui tutte le attività correnti devono essere sospese per la durata dello starnuto. È piuttosto fastidioso, soprattutto quando guidi e cerchi di sostenere una conversazione. Non vuoi scrivere codice che sia "sneezy".
Probabilmente hai utilizzato eventi e callback per aggirare questo 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 è per niente Eolo. 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
});
Questo non rileva le immagini che hanno generato un errore prima che avessimo la possibilità di ascoltarle; purtroppo il DOM non ci offre un modo per farlo. Inoltre, in questo modo viene caricata un'immagine. Le cose si complicano ulteriormente 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, come keyup, touchstart e così via. Con questi eventi, non ti interessa molto ciò che è successo prima di collegare il listener. Ma quando si tratta di successo/fallimento asincrono, l'ideale è avere 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 promise, ma con una denominazione migliore. Se gli elementi immagine HTML avessero un metodo "ready" che restituisce una promise, 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 promise sono un po' come i listener di eventi, con le seguenti differenze:
- Una promise può riuscire o non riuscire una sola volta. Non può riuscire o non riuscire due volte, né può passare da riuscito a non riuscito o viceversa.
- Se una promise è riuscita o non è riuscita e in un secondo momento aggiungi un callback di esito positivo/negativo, verrà chiamato il callback corretto, anche se l'evento si è verificato in precedenza.
Questo è estremamente utile per l'esito positivo/negativo asincrono, perché ti interessa meno il momento esatto in cui qualcosa è diventato disponibile e più la reazione al risultato.
Terminologia relativa alle promesse
Domenic Denicola ha corretto la prima bozza di questo articolo e mi ha dato una "F" per la terminologia. Mi ha messo in punizione, mi ha costretto a copiare Stati e destini 100 volte e ha scritto una lettera preoccupata ai miei genitori. Nonostante ciò, continuo a confondere molti termini, ma ecco le nozioni di base:
Una promessa può essere:
- fulfilled: l'azione relativa alla promise è andata a buon fine
- rejected: l'azione relativa alla promise non è riuscita
- pending: non ancora evaso o rifiutato
- settled: l'ordine è stato evaso o rifiutato
La specifica utilizza anche il termine thenable per descrivere un oggetto simile a una promise, in quanto ha un metodo then. Questo termine mi ricorda l'ex allenatore della nazionale di calcio inglese Terry Venables, quindi lo userò il meno possibile.
Le promise arrivano in JavaScript.
Le promise esistono da un po' di tempo sotto forma di librerie, ad esempio:
Le promesse di cui sopra e quelle di JavaScript condividono un comportamento comune e standardizzato chiamato Promises/A+. Se utilizzi jQuery, esiste un concetto simile chiamato Deferreds. Tuttavia, i Deferred non sono conformi a Promise/A+, il che li rende leggermente diversi e meno utili, quindi fai attenzione. jQuery ha anche un tipo Promise, ma questo è solo un sottoinsieme di Deferred e presenta gli stessi problemi.
Sebbene le implementazioni delle promise seguano un comportamento standardizzato, le loro API complessive differiscono. Le promise 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 della promise accetta un argomento, un callback con due parametri, resolve e reject. Esegui un'azione all'interno del callback, magari asincrona, 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 un'analisi 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 esito positivo e un altro per un caso di esito negativo. Entrambi sono facoltativi, quindi puoi aggiungere un callback solo per il caso di esito positivo o negativo.
Le promise JavaScript sono nate nel DOM come "Future", sono state rinominate in "Promise" e infine sono state spostate in JavaScript. Averli in JavaScript anziché nel DOM è un'ottima cosa perché saranno disponibili in contesti JS non browser come Node.js (se li utilizzano o meno nelle loro API di base è un'altra questione).
Sebbene siano una funzionalità di JavaScript, il DOM non ha paura di utilizzarle. In effetti, tutte le nuove API DOM con metodi di esito positivo/negativo asincroni utilizzeranno le promise. Questo sta già accadendo con Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams e altro ancora.
Compatibilità con altre librerie
L'API JavaScript Promises tratta qualsiasi elemento con un metodo then() come
simile a una promise (o thenable nel linguaggio delle promise sigh), quindi se utilizzi una libreria
che restituisce una promise Q, non c'è problema, funzionerà bene con le nuove
promise JavaScript.
Tuttavia, come ho già detto, i Deferred di jQuery sono un po'… inutili. Fortunatamente, puoi eseguirne il cast in promise standard, un'operazione che vale la pena eseguire il prima possibile:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
In questo caso, $.ajax di jQuery restituisce un oggetto Deferred. Poiché ha un metodo then(),
Promise.resolve() può trasformarlo in una promise JavaScript. Tuttavia, a volte gli oggetti Deferred passano più argomenti ai loro callback, ad esempio:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
Le promise JS, invece, ignorano tutto tranne il primo:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
Fortunatamente, questo è di solito ciò che vuoi o, almeno, ti dà accesso a ciò che vuoi. Inoltre, tieni presente che jQuery non segue la convenzione di passare oggetti Error nei rifiuti.
Codice asincrono complesso semplificato
Bene, scriviamo un po' di codice. Supponiamo di voler:
- Avvia un indicatore di rotazione per indicare il caricamento
- Recuperare un file JSON per una storia, che ci fornisce il titolo e gli URL di ogni capitolo
- Aggiungi un titolo alla pagina
- Recupera ogni capitolo
- Aggiungi la storia alla pagina
- Interrompi la rotazione
… ma anche di informare l'utente se qualcosa è andato storto durante il processo. A quel punto, dovremo anche interrompere lo spinner, altrimenti continuerà a girare, si confonderà e si scontrerà con un'altra UI.
Naturalmente, non utilizzeresti JavaScript per pubblicare una storia, la pubblicazione in HTML è più veloce, ma questo pattern è piuttosto comune quando si tratta di API: più recuperi di dati, poi fai qualcosa quando tutto è finito.
Per iniziare, occupiamoci del recupero dei dati dalla rete:
Promisifying XMLHttpRequest
Le API precedenti verranno aggiornate per utilizzare le promise, se possibile in modo compatibile con le versioni precedenti. 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'irritante notazione a cammello di XMLHttpRequest, più felice sarà la mia vita.
Concatenamento
then() non è la fine della storia, puoi concatenare i then per trasformare i valori o eseguire altre azioni asincrone una dopo l'altra.
Trasformazione dei 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 è in formato JSON, ma al momento la riceviamo come testo normale. Potremmo modificare la nostra funzione get per utilizzare il metodo JSON responseType, ma potremmo anche risolvere il problema nell'ambito delle promise:
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);
})
In effetti, potremmo creare una funzione getJSON() molto facilmente:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON() restituisce comunque una promise, che recupera un URL e poi analizza la risposta come JSON.
Metti in coda le azioni asincrone
Puoi anche concatenare i then per eseguire azioni asincrone in sequenza.
Quando restituisci un valore da un callback then(), è un po' magico.
Se restituisci un valore, il then() successivo viene chiamato con quel valore. Tuttavia, se restituisci un elemento simile a una promise, il then() successivo attende e viene chiamato solo quando la promise 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 effettuiamo una richiesta asincrona a story.json, che ci fornisce un insieme di URL da richiedere, quindi richiediamo il primo di questi. È qui che le promise iniziano davvero a distinguersi dai semplici pattern di callback.
Puoi 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 successiva che viene chiamato getChapter riutilizziamo la promise 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 fulfill e reject, nel linguaggio delle promise):
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'è nulla di speciale in catch(), è solo un'alternativa più leggibile a then(undefined, func). Tieni presente che i due esempi di codice precedenti non si comportano allo stesso modo, il secondo equivale a:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
La differenza è minima, ma estremamente utile. I rifiuti delle promise passano
al then() successivo con un callback di rifiuto (o catch(), poiché
è equivalente). Con then(func1, func2), verrà chiamato func1 o func2, mai entrambi. Tuttavia, con then(func1).catch(func2), verranno chiamati entrambi se func1 rifiuta, poiché sono passaggi separati nella catena. Considera
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" passano immediatamente al blocco catch(). Ecco quanto sopra sotto forma di diagramma di flusso (perché adoro i diagrammi di flusso):
Segui le linee blu per le promise che vengono soddisfatte o quelle rosse per quelle che vengono rifiutate.
Eccezioni e promise JavaScript
I rifiuti si verificano quando una promise 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 promise all'interno del callback del costruttore della promise, 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 va a buon fine (ad es. http 500 o l'utente è offline),
verranno ignorati tutti i callback di esito positivo successivi, incluso quello in
getJSON() che tenta di analizzare la risposta come JSON, e verrà ignorato anche il
callback che aggiunge chapter1.html alla pagina. Invece, passa al callback catch. Di conseguenza, se una delle azioni precedenti non è andata a buon fine, alla pagina verrà aggiunto il messaggio "Impossibile mostrare il capitolo".
Come per il blocco try/catch di JavaScript, l'errore viene rilevato e il codice successivo continua, quindi l'indicatore di rotazione è sempre nascosto, come vogliamo. Quanto sopra 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 recupero dall'errore. Per farlo, basta generare nuovamente 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 li vogliamo tutti. Facciamo in modo che ciò accada.
Parallelismo e sequenza: il meglio di 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 è sincrono e blocca il browser durante il download. Per far funzionare questo processo in modo asincrono, utilizziamo then() per fare in modo che le cose accadano 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 apparirebbero nell'ordine in cui vengono scaricati, che è fondamentalmente il modo in cui è stato scritto Pulp Fiction. Non siamo in Pulp Fiction, quindi sistemiamo le cose.
Creazione di una sequenza
Vogliamo trasformare il nostro array chapterUrls in una sequenza di promise. 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);
});
})
Questa è la prima volta che vediamo Promise.resolve(), che crea una promise che si risolve in qualsiasi valore le venga assegnato. Se passi un'istanza di Promise, la restituirà semplicemente (nota: si tratta di una modifica alla specifica che alcune implementazioni non seguono ancora). Se
passi un elemento simile a una promise (ha un metodo then()), crea una
Promise autentica che soddisfa/rifiuta nello stesso modo. Se passi un altro valore, ad esempio Promise.resolve('Hello'), viene creata una promise che viene soddisfatta con quel valore. Se la chiami senza valore, come sopra, viene soddisfatta con "undefined".
Esiste anche Promise.reject(val), che crea una promessa che rifiuta con il valore che le assegni (o undefined).
Possiamo riordinare il codice riportato sopra 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())
Questa operazione è la stessa dell'esempio precedente, ma non richiede la variabile "sequence" separata. Il nostro callback di riduzione viene chiamato per ogni elemento dell'array.
"sequence" è Promise.resolve() la prima volta, ma per il resto delle chiamate "sequence" è il valore restituito dalla chiamata precedente. array.reduce
è molto utile per ridurre un array a un singolo valore, che in questo caso
è una promise.
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';
})
Ecco fatto, una versione completamente asincrona della versione sincrona. 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 stiamo perdendo prestazioni scaricando i capitoli uno dopo l'altro. Quello che vogliamo fare è scaricarli tutti contemporaneamente e poi elaborarli quando sono arrivati tutti. Fortunatamente esiste un'API per questo:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all accetta un array di promise e ne crea una che viene soddisfatta quando tutte vengono completate correttamente. Ricevi un array di risultati (qualsiasi cosa
le promise abbiano restituito) nello stesso ordine delle promise 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 alcuni 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 arriva il primo capitolo, dobbiamo aggiungerlo alla pagina. In questo modo, l'utente può iniziare a leggere prima che arrivino gli altri capitoli. Quando arriva il capitolo tre, non lo aggiungiamo alla pagina perché l'utente potrebbe non rendersi conto che manca il capitolo due. Quando arriva il capitolo due, possiamo aggiungere i capitoli due e tre e così via.
Per farlo, recuperiamo il JSON per tutti i capitoli contemporaneamente, 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';
})
Ecco fatto, il meglio di entrambi i mondi. La pubblicazione di tutti i contenuti richiede lo stesso tempo, ma l'utente riceve la prima parte dei 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 descritte sopra con callback o eventi in stile Node.js richiede circa il doppio del codice, ma soprattutto non è così facile da seguire. Tuttavia, questa non è la fine della storia per le promise, se combinate con altre funzionalità di ES6 diventano ancora più semplici.
Gara bonus: funzionalità ampliate
Da quando ho scritto questo articolo, la possibilità di utilizzare le Promise si è ampliata notevolmente. A partire da Chrome 55, le funzioni asincrone consentono di scrivere codice basato su promise come se fosse sincrono, ma senza bloccare il thread principale. Puoi scoprire di più in merito nel mio articolo sulle funzioni asincrone. Le promise e le funzioni asincrone sono ampiamente supportate nei principali browser. Puoi trovare i dettagli nella documentazione di riferimento di MDN relativa a Promise e alla funzione async.
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, ringraziamo Mathias Bynens per aver aggiornato varie parti dell'articolo.