JavaScript Promise: introduzione

Le promesse semplificano i calcoli differiti e asincroni. Una promessa rappresenta un'operazione non ancora completata.

Jake Archibald
Jake Archibald

Preparati a un momento cruciale nella storia per lo sviluppo web.

[Inizio del rullo di tamburi]

Le promesse sono arrivate in JavaScript!

[I fuochi d'artificio esplodono, la carta scintillante piove dall'alto, la folla impazzisce]

A questo punto, rientri in una di queste categorie:

  • Alcune persone fanno il tifo per te, ma non sai quale sia la confusione informazioni. Forse non sai quale "promessa" dall'indirizzo IP interno. Faresti le spalle, ma un peso di carta glitterata ti appesantisce le spalle. Se sì, non preoccuparmi, ci sono voluti anni per capire perché dovrei preoccuparmi cose nuove. È consigliabile iniziare dall'inizio.
  • Sei forte! Era l'ora, giusto? Hai già usato questi elementi Promise in passato ma ci dà fastidio che tutte le implementazioni hanno un'API leggermente diversa. Che cos'è l'API per la versione JavaScript ufficiale? Probabilmente vorrai iniziare con la terminologia.
  • Lo sapevate già, e scherzavo con chi sta saltando e come se fossero notizie per loro. Prenditi un momento per crogiolarti nella tua propria superiorità, quindi vai direttamente al riferimento API.

Supporto del browser e polyfill

Supporto dei browser

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Origine

Per portare alle specifiche i browser che non hanno un'implementazione completa conformità o di aggiungere promesse ad altri browser e Node.js, controlla il polyfill (2 kB compressi).

Qual è il problema?

JavaScript è a thread singolo, il che significa che non possono essere eseguiti due bit di script contemporaneamente; devono essere eseguiti uno dopo l'altro. Nei browser, JavaScript condivide un thread con un carico di altri elementi che differiscono da browser a del browser. Ma in genere JavaScript si trova nella stessa coda del disegno, stili e gestione delle azioni dell'utente (ad esempio, l'evidenziazione del testo e l'interazione con i controlli del modulo). L'attività in uno di questi elementi ritarda le altre.

In quanto essere umano, il tuo account è multithread. Puoi digitare con più dita, puoi guidare e tenere una conversazione contemporaneamente. L'unico blocco funzione che dobbiamo affrontare è starnutire, dove tutta l'attività corrente deve sospeso per la durata dello starnuto. È piuttosto fastidioso, soprattutto quando guidi e cerchi di sostenere una conversazione. Non devi quando vuoi scrivere codice sospeso.

Probabilmente, avrai utilizzato gli eventi e i callback per aggirare questa limitazione. 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 subdolo. Otteniamo l'immagine, aggiungiamo un paio di listener e L'esecuzione di JavaScript può essere interrotta fino alla chiamata di uno di questi listener.

Purtroppo, nell'esempio precedente, è possibile che gli eventi si siano verificati prima di iniziare ad ascoltarli, quindi dobbiamo risolvere il problema usando il valore "complete" proprietà 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
});

Questa funzione non rileva immagini con errori prima che abbiamo avuto la possibilità di esaminarle them; sfortunatamente il DOM non ci consente di farlo. Inoltre, caricamento di un'immagine. Le cose diventano ancora più complesse se vogliamo sapere quando un insieme di immagini caricate.

Gli eventi non sono sempre il modo migliore

Gli eventi sono ideali per eventi che possono verificarsi più volte durante la stessa sessione. oggetto: keyup, touchstart e così via. Con questi eventi non ti interessano davvero su ciò che è accaduto prima di collegare il listener. Ma quando si tratta di successo/errore asincrono, idealmente dovresti ottenere qualcosa del genere:

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 "pronto" che ha restituito una promessa, potremmo fare questo:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

In linea di massima, le promesse sono un po' come gli ascoltatori di eventi, ad eccezione di:

  • Una promessa può avere successo o fallimento solo una volta. Non può avere esito positivo o negativo due volte, né passare dal successo all'insuccesso o viceversa.
  • Se una promessa ha avuto esito positivo o negativo e in un secondo momento aggiungi un esito positivo o negativo viene chiamato il callback corretto, anche se l'evento ha richiesto posto in precedenza.

Ciò è estremamente utile per l'esito positivo o negativo in caso di esito negativo, perché interessato al momento esatto in cui un prodotto diventava disponibile e più interessato nella reazione al risultato.

Terminologia delle promesse

Prova di Domenic Denicola letta la prima bozza di questo articolo e mi ha assegnato un voto "F" per la terminologia. Mi ha arrestato mi ha costretto a copiare Stati e destini 100 volte e ha scritto una lettera preoccupata ai miei genitori. Nonostante ciò, mischiamo gran parte della terminologia, ma ecco le nozioni di base:

Una promessa può essere:

  • completata: l'azione relativa alla promessa è stata completata
  • rejected: l'azione relativa alla promessa non è andata a buon fine
  • in sospeso - Non è stato ancora completato o rifiutato
  • regolamentato: è stato evaso o rifiutato

Specifiche usa anche il termine thenable per descrivere un oggetto simile a una promessa, in quanto ha un metodo then. Questo termine mi ricorda l'ex Football americano Gestore Terry Venables in modo La userò il meno possibile.

Le promesse vengono arrivate in JavaScript

È da un po' che esistono promesse che esistono sotto forma di biblioteche, ad esempio:

Le promesse riportate sopra e JavaScript condividono un comportamento comune e standardizzato chiamata Promises/A+. Se sei un utente di jQuery, hanno una stringa simile chiamata Rinviate. Tuttavia, I differiti non sono conformi a Promise/A+, il che li rende in modo leggermente diverso e meno utile, quindi fai attenzione. jQuery ha anche un tipo Promise, ma questo è solo un sottoinsieme di differito e presenta gli stessi problemi.

Sebbene le implementazioni delle promesse seguano un comportamento standardizzato, le API in generale differiscono. Le promesse relative a JavaScript sono simili nell'API a quelle in 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 promessa prende un argomento, un callback con due parametri, risolvere e rifiutare. Esegui un'azione all'interno del callback, ad esempio asincrono, quindi chiama risolvi se tutto ha funzionato, altrimenti chiama Rifiuta.

Come throw nel vecchio codice JavaScript, è consuetudine, ma non obbligatoria, da rifiutare con un oggetto Error. Il vantaggio degli oggetti Error è che catturano l'analisi dello stack, rendendo più utili gli strumenti di debug.

Ecco come puoi usare 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 per solo in caso di successo o fallimento.

Le promesse di JavaScript sono iniziate nel DOM con il nome "Futures", ribattezzata "Promises" e infine sono passati a JavaScript. Averli in JavaScript anziché Il DOM è ottimo perché sarà disponibile in contesti JS non browser come Node.js (un'altra domanda è se li utilizzano nelle loro API principali).

Nonostante siano una funzionalità JavaScript, il DOM non ha paura di usarle. Nella Infatti, tutte le nuove API DOM con metodi asincroni di successo/errore utilizzeranno le promesse. Ciò sta già accadendo con Gestione delle quote, Eventi di caricamento dei caratteri, ServiceWorker, Web MIDI, Stream e altro ancora.

Compatibilità con altre librerie

L'API JavaScript Promesse tratterà qualsiasi elemento con un metodo then() come (o thenable in sospiro di promessa), quindi se usi una libreria che restituisce una promessa Q, va bene, va bene con le nuove JavaScript promette.

Anche se, come ho detto, i differiti di jQuery sono un po'... inutili. Per fortuna, puoi trasmetterle alle promesse standard, cosa che vale la pena fare il prima possibile:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

In questo caso, $.ajax di jQuery restituisce Deferred. Poiché ha un metodo then(), Promise.resolve() può trasformarlo in una promessa JavaScript. Tuttavia, a volte differisce il passaggio di più argomenti ai propri callback, ad esempio:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

JS promette di ignorare tutto tranne il primo:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

Per fortuna di solito è quello che vuoi o almeno ti dà accesso quello che vuoi. Inoltre, tieni presente che jQuery non segue la convenzione di passando gli oggetti Error in rifiuti.

Semplificazione del codice asincrono complesso

Esatto, proviamo a programmare alcune cose. Supponiamo di voler:

  1. Avvia una rotellina per indicare il caricamento
  2. Recupera JSON per una storia, che fornisce il titolo e gli URL di ogni capitolo
  3. Aggiungi il titolo alla pagina
  4. Recupera ogni capitolo
  5. Aggiungi la notizia alla pagina
  6. Arresta la rotellina

... ma segnala anche all'utente se si è verificato un problema durante il processo. Vogliamo per fermare la rotellina anche a quel punto, altrimenti continua a girare, stordito e si arresta in modo anomalo in un'altra UI.

Naturalmente, non useresti 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: più dati vengono recuperati, quindi fai qualcosa quando hai finito.

Per iniziare, vediamo come recuperare i dati dalla rete:

Promessa di XMLHttpRequest

Le API precedenti verranno aggiornate in modo da utilizzare le promesse, se possibile in modo compatibile. XMLHttpRequest è un candidato primario, ma nel frattempo scriviamo una funzione semplice 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 usiamolo:

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 è ottimo, perché meno non vedo l'inconveniente di cammello di XMLHttpRequest, più sarà felice la mia vita.

Concatenamento

then() non è la fine della storia, puoi concatenare then 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 è in formato JSON, ma al momento la stiamo ricevendo in testo normale. Me potrebbe modificare la nostra funzione get per utilizzare il codice JSON responseType, ma potremmo anche risolverlo nella terra delle promesse:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Poiché JSON.parse() prende 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);
})

Di fatto, potremmo creare una funzione getJSON() molto facilmente:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() restituisce comunque una promessa, ovvero una che recupera un URL e poi analizza la risposta in formato JSON.

Coda delle 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, il prossimo then() lo aspetta e viene chiamato solo quando la promessa si risolve (riuscito/non funziona). Ad esempio:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

Qui facciamo una richiesta asincrona a story.json, che ci fornisce un insieme URL da richiedere, richiediamo il primo di questi. È qui che le promesse a distinguersi dai semplici pattern di callback.

Puoi anche creare una scorciatoia per visualizzare 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 fino a quando non verrà chiamato getChapter, ma la prossima volte getChapter: riutilizziamo la promessa per la 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 il successo, uno per gli errori (o soddisfare e rifiutare, in parole 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 codici gli esempi precedenti non si comportano nello 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 è sottile, ma estremamente utile. Ignora la promessa di rifiuto inoltra al successivo then() con un callback di rifiuto (o catch(), è equivalente). Con then(func1, func2), func1 o func2 saranno chiamate, mai entrambe. Ma con then(func1).catch(func2), entrambe le azioni chiamato se func1 rifiuta, poiché si tratta di passaggi separati nella catena. Prendi le seguenti:

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 ai normali test/catch di JavaScript, errori che avvengono con un comando "try" vai subito al blocco catch(). Ecco il sopra come diagramma di flusso (perché adoro i diagrammi di flusso):

Segui le linee blu per le promesse che vengono rispettate e quelle rosse per quelle che rifiutare.

Eccezioni e promesse relative a 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 svolgere tutte le attività relative alle promesse all'interno del promettono il callback del costruttore, così gli errori vengono rilevati diventano 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 nella pratica

Con la storia e i capitoli, possiamo usare la funzionalità Cattura 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), ignorerà tutti i seguenti callback riusciti, tra cui quello in getJSON() che tenta di analizzare la risposta come JSON e ignora anche il che aggiunge capitolo1.html alla pagina. ma passa alla di Google. Di conseguenza, viene visualizzato il messaggio "Impossibile mostrare il capitolo" verrà aggiunto alla pagina se nessuna delle azioni precedenti non è andata a buon fine.

Come nel caso di trial/catch di JavaScript, l'errore viene rilevato e il codice successivo continua, quindi la rotellina è sempre nascosta, che è ciò che vogliamo. La 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'

Ti consigliamo di catch() semplicemente per scopi di logging, senza recuperare dall'errore. Per farlo, è sufficiente restituire l'errore. Potremmo farlo in il 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. Creiamo che accadono.

Parallelismo e sequenziamento: ottenere il meglio da entrambi

Pensare in modo asincrono non è facile. Se fai fatica a lasciare il segno, 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'

Va bene! Trasmette invece la sincronizzazione e blocca il browser durante il download. A per fare in modo che il lavoro sia asincrono utilizziamo then() per far sì che le cose si verifichino 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 è asincrono, quindi i nostri capitoli appariranno in qualsiasi ordine scaricano, che è fondamentalmente come è stato scritto PulpFiction. Non è Pulp Narrativa, 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(), il che ha creato una promessa che si risolve a qualsiasi valore da te attribuitole. Se la superi di Promise, lo restituirà semplicemente (nota: questo è un alla specifica che alcune implementazioni non seguono ancora). Se le passino qualcosa come una promessa (ha un metodo then()), crea un Promise originale che soddisfa/rifiuta nello stesso modo. Se superi in qualsiasi altro valore, ad esempio Promise.resolve('Hello'), crea un una promessa che soddisfi questo valore. Se la chiami senza valore, come sopra, si completa con "undefined".

C'è anche Promise.reject(val), che crea una promessa che rifiuta con il valore che gli assegni (o non definito).

Possiamo mettere ordine nel codice riportato sopra 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 funziona come nell'esempio precedente, ma non richiede la codifica "sequenza" . Il nostro callback di riduzione viene chiamato per ogni elemento nell'array. "sequenza" è Promise.resolve() la prima volta, ma per il resto della chiama "sequenza" è il risultato della chiamata precedente. array.reduce è molto utile per ridurre un array a un singolo valore, che in questo caso è una promessa.

Riepiloghiamo il tutto:

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';
})

Eccola, una versione completamente asincrona della versione di sincronizzazione. Ma possiamo farlo meglio. Al momento la nostra pagina viene scaricata nel seguente modo:

I browser sono abbastanza bravi a scaricare più contenuti contemporaneamente, quindi perdiamo le prestazioni scaricando i capitoli uno dopo l'altro. Quello che vogliamo fare è scaricali tutti contemporaneamente, per poi elaborarli quando sono tutti arrivati. Per fortuna esiste un'API per questa operazione:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all accetta una serie di promesse e crea una promessa che si mantiene quando tutti i passaggi sono stati completati correttamente. Otterrai una serie di risultati (di qualsiasi le promesse rispettate) nello stesso ordine delle promesse fatte.

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';
})

In base alla connessione, questo può essere secondi più veloce rispetto al caricamento di file uno alla volta, ed è meno codice del primo tentativo. I capitoli possono essere scaricati ma appaiono sullo schermo nell'ordine corretto.

Tuttavia, possiamo comunque migliorare il rendimento percepito. Quando arriva il primo capitolo, dovrebbe aggiungerlo alla pagina. In questo modo l'utente potrà iniziare a leggere prima del resto sono arrivati i capitoli. Al capitolo tre, non lo aggiungeremo perché l'utente potrebbe non rendersi conto che manca il capitolo due. Quando il capitolo due possiamo aggiungere i capitoli 2, 3 e così via.

Per farlo, recuperiamo il file JSON per tutti i capitoli contemporaneamente, quindi creiamo per aggiungerle 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! La distribuzione richiede la stessa quantità di tempo tutti i contenuti, ma l'utente riceve prima la prima parte.

In questo banale esempio, tutti i capitoli arrivano più o meno contemporaneamente, ma il vantaggio di mostrarne una alla volta sarà esagerato con offerte i capitoli.

Come spiegato in precedenza, con i callback in stile Node.js eventi intorno a il doppio del codice, ma soprattutto non è così facile da seguire. Tuttavia, questo non termina la storia delle promesse, se abbinate ad altre funzionalità di ES6. diventano ancora più facili.

Gara bonus: possibilità estese

Da quando ho scritto inizialmente questo articolo, la possibilità di usare Promise è stata ampliata molto. A partire da Chrome 55, le funzioni asincrone hanno consentito la pubblicazione di codice basato su promesse scritto come se fosse sincrono, ma senza bloccare il thread principale. Puoi Per saperne di più, leggi l'articolo sulle mie funzioni asincrone. C'è supporto diffuso delle funzioni Promises e asinc nei principali browser. Puoi trovare i dettagli negli MDN Promessa e asinc riferimento.

Grazie mille ad Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans e Yutaka Hirano, che hanno corretto la bozza e realizzato correzioni/consigli.

Inoltre, grazie a Mathias Bynens per aggiornando le varie parti dell'articolo.