Promesas de JavaScript: introducción

Las promesas simplifican los cálculos diferidos y asíncronos. Una promesa representa una operación que aún no se completó.

Desarrolladores, prepárense para un momento crucial en la historia de el desarrollo web.

[Redoble de tambores]

Llegaron las promesas a JavaScript.

[Fuegos artificiales, lluvia de papeles y exaltación de la multitud]

En este momento, te encuentras en una de estas categorías:

  • Hay personas que te alientan a ti, pero no sabes de qué se trata todo el alboroto sobre el tema. Tal vez no estés seguro de lo que es una "promesa" en la nube. Te encogerías de hombros, pero peso de papel reluciente se te pesa. Si es así, no Preocuparme por eso, me tomó años descubrir por qué todo esto debería importarme. cosas. Es probable que te convenga comenzar por el principio.
  • Te embarga la alegría. Ya era hora, ¿verdad? Ya usaste estas funciones de la promesa pero le molesta que todas las implementaciones tengan una API un poco diferente. ¿Cuál es la API de la versión oficial de JavaScript? Es probable que quieras empezar con la terminología.
  • Ya sabías esto y te burlas de los que están saltando como si fueran noticias para ellos. Tómate un momento para disfrutar de tu propia superioridad, Luego, dirígete directamente a la referencia de la API.

Compatibilidad con navegadores y polyfill

Navegadores compatibles

  • Chrome: 32.
  • Límite: 12.
  • Firefox: 29.
  • Safari: 8.

Origen

Para llevar a las especificaciones los navegadores que no tienen una implementación completa de promesas. o agregar promesas a otros navegadores y Node.js, consulta el polyfill (2,000 comprimidos).

¿Por qué tanto escándalo?

JavaScript es de un solo subproceso, es decir, no se pueden ejecutar dos fragmentos de secuencia de comandos en al mismo tiempo; tienen que ejecutarse una tras otra. En navegadores, JavaScript Comparte un hilo con muchos otros elementos que difieren de un navegador a otro navegador. Pero, por lo general, JavaScript está en la misma cola que la pintura, estilos y el manejo de las acciones del usuario (como destacar texto e interactuar con los controles de formularios). La actividad en uno de estos elementos retrasa los demás.

Como ser humano, tienes varios subprocesos. Puedes escribir con varios dedos, puedes conducir y mantener una conversación al mismo tiempo. El único bloqueo función es el estornudo, donde toda la actividad actual debe suspenderse por un estornudo. Eso es bastante molesto, especialmente cuando conduces y tratas de mantener una conversación. No y quieres escribir código que provoque estornudos.

Probablemente hayas usado eventos y devoluciones de llamada para evitar esto. Estos son los eventos:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

No hay estornudos. Obtenemos la imagen, agregamos algunos objetos de escucha y, luego, JavaScript puede dejar de ejecutarse hasta que se llame a uno de esos objetos de escucha.

En el ejemplo anterior, es posible que los eventos hayan ocurrido. antes de comenzar a escucharlos, así que debemos solucionarlo el estado "completo" propiedad de las imágenes:

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

No se detectan las imágenes que generaron un error antes de que pudiéramos escuchar las them; lamentablemente, el DOM no nos brinda una forma de hacerlo. Además, este es cargando una imagen. El proceso se vuelve aún más complejo si queremos saber cuándo un conjunto de imágenes.

Los eventos no siempre son la mejor manera

Los eventos son excelentes cuando suceden varias veces la misma objeto (keyup, touchstart, etc.). Con esos eventos, realmente no te interesan sobre lo que sucedió antes de adjuntar el objeto de escucha. Pero cuando se trata de éxito/error asíncrono, idealmente, querrás algo como esto:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

Esto es lo que hacen las promesas, pero con una mejor nomenclatura. Si los elementos de imagen HTML tuvieran un "listo" que muestre una promesa, podemos hacer lo siguiente:

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

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

Básicamente, las promesas se parecen a los objetos de escucha de eventos, a excepción de lo siguiente:

  • Una promesa solo puede completarse con éxito o fallar una vez. No puede tener éxito o fallar dos veces, ni puede pasar de exitosa a con errores ni viceversa.
  • Si una promesa se completó correctamente o falló y luego agregas un error o un error de devolución de llamada, se llamará a la devolución de llamada correcta, aunque el evento tarde el sitio anterior.

Esto es muy útil para el éxito o fracaso de procesos asíncronos, interesado en el momento exacto en que algo estuvo disponible y más interesado en reaccionar al resultado.

Terminología de las promesas

Domenic Denicola leyó el primer borrador. de este artículo y me calificaron con "F" para la terminología. Me puso en detención, me obligó a copiar Estados y destinos 100 veces y escribí una carta preocupada a mis padres. A pesar de eso, sigo se confunde la terminología, pero estos son los conceptos básicos:

Una promesa puede ser:

  • fulfill: la acción relacionada con la promesa se realizó correctamente
  • rejected: No se pudo realizar la acción relacionada con la promesa.
  • pending: Aún no se completa ni se rechaza.
  • liquidado: Se completó o se rechazó.

Las especificaciones también usa el término thenable para describir un objeto parecido a una promesa ya que tiene un método then. Este término me recuerda a la futbol americano de Inglaterra. administrador de Terry Venables Lo usaré lo menos posible.

¡Llegaron las promesas a JavaScript!

Las promesas existen desde hace tiempo en forma de bibliotecas, como las siguientes:

Lo anterior y las promesas de JavaScript tienen en común un comportamiento estandarizado llamada Promises/A+. Si usuario de jQuery, tienen algo similar llamado Aplazados. Sin embargo, Los diferidos no cumplen con Promise/A+, por lo que se trata de algo diferente y menos útil, así que ten cuidado. jQuery también tiene un tipo de promesa, pero esto es solo un subconjunto de Deferred y tiene los mismos problemas.

Si bien las implementaciones de las promesas siguen un comportamiento estandarizado, las APIs generales difieren. En cuanto a la API, las promesas de JavaScript son similares a las de confirmación de asistencia.js. Así se crea una promesa:

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

El constructor de la promesa recibe un argumento: una devolución de llamada con dos parámetros. resolver y rechazar. Haz algo en la devolución de llamada, tal vez de forma asíncrona, y luego llama resolver si todo funcionó; de lo contrario, llama a rechazar.

Al igual que throw en el JavaScript que todos conocemos, es costumbre, pero no obligatoria, rechazar con un objeto Error. El beneficio de los objetos Error es que capturan seguimiento de pila, lo que hace que las herramientas de depuración sean más útiles.

Así es como se usa esa promesa:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() toma dos argumentos, una devolución de llamada para un caso de éxito y otro. para el caso de falla. Ambos son opcionales, de modo que puedes agregar una devolución de llamada para el solo para casos de éxito o fracaso.

Las promesas de JavaScript empezaron en DOM como "Future" y se les cambió el nombre a "Promise", y, por último, se trasladará a JavaScript. Tenerlas en JavaScript en lugar de en el DOM es muy bueno porque estarán disponibles en contextos de JS sin navegador, como sobre Node.js (si los usan en sus APIs principales es otra cuestión).

Si bien son una función de JavaScript, el DOM no tiene miedo de usarlas. En De hecho, todas las nuevas APIs de DOM con métodos de éxito o falla asíncronos usarán promesas. Esto ya está sucediendo con Administración de cuotas, Eventos de carga de fuentes, ServiceWorker MIDI web, Transmisiones y mucho más

Compatibilidad con otras bibliotecas

La API de las promesas de JavaScript tratará todo lo que tenga un método then() como como una promesa (o thenable en un suspiro de promesas), por lo que si usas una biblioteca que devuelva una promesa Q, está bien, funcionará bien con la nueva promesas de JavaScript.

Sin embargo, como mencioné, los elementos Deferred de jQuery son un poco... inútiles. Afortunadamente, puedes transmitirlos a las promesas convencionales, lo que vale la pena lo antes posible:

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

Aquí, $.ajax de jQuery muestra un elemento Deferred. Como tiene un método then(), Promise.resolve() puede convertirlo en una promesa de JavaScript. Sin embargo, En ocasiones, los elementos deferred pasan varios argumentos a sus devoluciones de llamada, por ejemplo:

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

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

En cambio, las promesas de JS ignoran todo menos el primero:

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

Afortunadamente, esto suele ser lo que quieres o al menos te da acceso lo que quieres. Además, ten en cuenta que jQuery no sigue la convención de y pasar objetos Error a rechazos.

Código asíncrono complejo más fácil

Bueno, codifiquemos algunas cosas. Supongamos que queremos hacer lo siguiente:

  1. Inicia un ícono giratorio para indicar que se está cargando
  2. Recuperar algunos datos JSON para una historia, que nos proporciona el título y las URL de cada capítulo
  3. Agrega un título a la página
  4. Obtener cada capítulo
  5. Agrega la historia a la página
  6. Detener el ícono giratorio

... pero también decirle al usuario si algo salió mal en el camino. Querremos para detener el ícono giratorio en ese punto. De lo contrario, seguirá girando, y chocar con alguna otra IU.

Por supuesto, no usarías JavaScript para entregar una historia, publicar como HTML es más rápido, pero este patrón es muy común cuando se trabaja con APIs: varios datos y hace algo cuando todo esté listo.

Para comenzar, analicemos la recuperación de datos de la red:

Promesas en XMLHttpRequest

Las APIs antiguas se actualizarán para usar promesas y, si es posible, de una forma compatible. XMLHttpRequest es un candidato principal, pero mientras tanto escribamos una función simple para hacer una solicitud 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();
  });
}

Ahora lo usaremos:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

Ahora podemos hacer solicitudes HTTP sin escribir XMLHttpRequest de forma manual, lo cual es genial, ya que menos tendré que ver la exasperante prenda en camello de XMLHttpRequest, más feliz será mi vida.

Encadenamiento

then() no es el final de la historia. Puedes encadenar then para transformar valores o ejecutar acciones asíncronas adicionales una tras otra.

Cómo transformar valores

Puedes transformar valores con solo mostrar el valor nuevo:

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

Como ejemplo práctico, volvamos a lo siguiente:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

La respuesta es JSON, pero la recibimos como texto sin formato. Mié podría alterar la función GET para usar el archivo JSON responseType: pero también podríamos resolverlo en el país de las promesas:

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

Dado que JSON.parse() toma un solo argumento y muestra un valor transformado, podemos crear un atajo:

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

De hecho, podríamos hacer una función getJSON() con mucha facilidad:

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

getJSON() sigue mostrando una promesa, una que recupera una URL y, luego, la analiza. la respuesta como JSON.

Pon en cola acciones asíncronas

También puedes encadenar objetos then para ejecutar acciones asíncronas en secuencia.

Cuando muestras algo de una devolución de llamada de then(), es un poco mágico. Si muestras un valor, se llama al siguiente then() con ese valor. Sin embargo, Si muestras algo parecido a una promesa, el siguiente then() lo esperará y se solo se llama cuando la promesa se termina (se completa con éxito/falla). Por ejemplo:

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

Aquí hacemos una solicitud asíncrona a story.json, que nos proporciona un conjunto de URL que se deben solicitar, luego solicitamos la primera de ellas. Aquí es donde las promesas comienza a destacarse frente a los patrones de devolución de llamada simples.

Incluso puedes crear un método abreviado para obtener capítulos:

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

No descargaremos story.json hasta que se llame a getChapter, pero la siguiente vez que se llama getChapter, reutilizamos la promesa de la historia, por lo que story.json solo se recupera una vez. ¡Viva las promesas!

Manejo de errores

Como vimos antes, then() toma dos argumentos: uno de éxito y otro en caso de fallas (o cumplir y rechazar, en palabras de lo prometido):

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

También puedes usar catch():

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

El catch() no tiene nada de especial, es solo azúcar. then(undefined, func), pero es más legible. Ten en cuenta que los dos códigos ejemplos anteriores no se comportan de la misma manera, este último equivale a lo siguiente:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

La diferencia es sutil, pero extremadamente útil. Omitir las promesas rechazadas reenviar al siguiente then() con una devolución de llamada de rechazo (o catch(), ya que son equivalentes). Con then(func1, func2), se realizará func1 o func2 nunca se llama a ambas. Sin embargo, con then(func1).catch(func2), se realizarán ambas acciones Se llama si se rechaza func1, ya que son pasos separados de la cadena. Toma lo siguiente:

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!");
})

El flujo anterior es muy similar al try/catch de JavaScript normal, que contiene errores que suceder dentro de una “prueba” ve inmediatamente al bloque catch(). Este es el arriba como un diagrama de flujo (porque me encantan estos diagramas):

Sigue las líneas azules para las promesas que se cumplan o las rojas para las que se cumplan rechazar.

Excepciones y promesas de JavaScript

Los rechazos ocurren cuando una promesa se rechaza de forma explícita, pero también de forma implícita Si se produce un error en la devolución de llamada del constructor:

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

Esto significa que es útil hacer todo el trabajo relacionado con promesas de constructor de promesa para que los errores se detecten automáticamente y convertirse en rechazos.

Lo mismo ocurre con los errores arrojados en devoluciones de llamada de 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);
})

Manejo de errores en la práctica

Con nuestra historia y los capítulos, podemos usar catch para mostrar un error al usuario:

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

Si falla la recuperación de story.chapterUrls[0] (p.ej., http 500 o el usuario está sin conexión), se omitirán todas las siguientes devoluciones de llamada de éxito, incluidas la que se encuentra en getJSON(), que intenta analizar la respuesta como JSON y también omite el que agregue capítulo1.html a la página. En cambio, se mueve devolución de llamada. Como resultado, aparece el mensaje "No se pudo mostrar el capítulo". se agregará a la página si Falló alguna de las acciones anteriores.

Como try/catch de JavaScript, se detecta el error y el código posterior continúa, por lo que el ícono giratorio siempre está oculto, que es lo que queremos. El anterior se convierte en una versión asincrónica y sin bloqueo de lo siguiente:

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'

Es posible que quieras catch() solo para fines de registro, sin recuperar del error. Para ello, simplemente arroja el error. Podríamos hacer esto en nuestro método getJSON():

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

Hemos podido obtener un capítulo, pero los queremos a todos. Hagamos que ocurren.

Paralelismo y secuencia: cómo aprovechar lo mejor de ambos

No es fácil pensar como asincrónico. Si te cuesta dar el primer paso, intenta escribir el código como si fuera síncrono. En este caso, ocurre lo siguiente:

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'

Sí, funciona. Sin embargo, es sincrónico y bloquea el navegador mientras se descargan las cosas. Para Para que este trabajo sea asíncrono, usamos then() para que las cosas sucedan una tras otra.

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

Pero ¿cómo podemos recorrer las URLs de los capítulos y recuperarlas en orden? Esta no funciona:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach no admite el formato asíncrono, por lo que nuestros capítulos aparecerán en el orden que corresponda. que descargan, que es básicamente la forma en que se redactó el guión de Pulp Subscribe. No es El pulso no alcanza.

Crea una secuencia

Queremos convertir nuestro array chapterUrls en una secuencia de promesas. Podemos hacerlo con 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);
  });
})

Esta es la primera vez que vemos Promise.resolve(), que crea un que se resuelva al valor que le das. Si le pasas un instancia de Promise, solo la mostrará. (Nota: Esta es una cambiar la especificación que algunas implementaciones aún no siguen). Si le pasa algo parecido a una promesa (tiene un método then()), crea un Promise genuina que cumple o rechaza de la misma manera. Si apruebas en cualquier otro valor, p.ej., Promise.resolve('Hello'), crea un que se cumple con ese valor. Si la llamas sin valor, como se muestra arriba, se completa con "undefined".

También existe Promise.reject(val), que crea una promesa que se rechaza con el valor que le asignas (o no definido).

Podemos ordenar el código anterior usando 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())

Cumple la misma función que el ejemplo anterior, pero no requiere la separación "secuencia" de salida. Llamamos a la devolución de llamada "reduce" para cada elemento de la matriz. "secuencia" es Promise.resolve() la primera vez, pero para el resto de llama a "secuencia" es el resultado de la llamada anterior. array.reduce es muy útil para reducir una matriz a un solo valor, que en este caso es una promesa.

Juntemos todo:

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

Y ahí la tenemos, una versión completamente asincrónica de la versión síncrona. Pero podemos hacer mejor. En este momento, nuestra página realiza descargas de la siguiente forma:

Los navegadores son bastante buenos para descargar varias cosas al mismo tiempo, así que y el rendimiento descargando los capítulos uno tras otro. Lo que queremos hacer es descargarlos al mismo tiempo y procesarlos cuando todos hayan terminado de descargarse. Afortunadamente, existe una API para esto:

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

Promise.all toma un array de promesas y crea una que cumpla con cuando todos se completen con éxito. Obtienes un array de resultados (independientemente las promesas cumplidas) en el mismo orden que las promesas que pasaste.

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

Dependiendo de la conexión, esto puede ser segundos más rápido que cargar una por una, y tiene menos código que nuestro primer intento. Los capítulos se pueden descargar pero aparecen en la pantalla en el orden correcto.

Sin embargo, aún podemos mejorar el rendimiento percibido. Cuando llega el capítulo uno, debes agregarlo a la página. Esto le permite al usuario comenzar a leer antes que el resto de llegaron los capítulos. Cuando llega el capítulo tres, no lo agregamos a porque es posible que el usuario no se dé cuenta de que falta el capítulo dos. Cuando el capítulo dos llega el capítulo dos, tres, etcétera.

Para ello, recuperamos el archivo JSON de todos los capítulos al mismo tiempo y, luego, creamos un para agregarlos 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';
})

Aquí vamos, lo mejor de ambos. Tarda lo mismo en entregarse todo el contenido, pero el usuario obtiene la primera parte de este antes.

En este ejemplo trivial, todos los capítulos llegan casi al mismo tiempo, pero el beneficio de mostrar uno a la vez se exagerará con más imágenes capítulos.

Si haces lo anterior con devoluciones de llamada de estilo Node.js o de eventos está cerca duplicar el código, pero lo que es más importante, no es tan fácil de seguir. Sin embargo, no es el fin de la historia de las promesas, cuando se combina con otras funciones de ES6 se vuelven aún más fáciles.

Ronda adicional: capacidades expandidas

Desde que escribí este artículo originalmente, la capacidad de usar promesas se ha expandido de manera significativa. A partir de Chrome 55, las funciones asincrónicas permiten que se pueda escrita como si fuera síncrono, pero sin bloquear el subproceso principal. Puedes obtén más información al respecto en mi artículo sobre funciones asíncronas. Hay compatibilidad generalizada con promesas y funciones asincrónicas en los principales navegadores Puedes encontrar los detalles en los MDN Promesa y la función async función referencia.

Muchas gracias a Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans y Yutaka Hirano, quienes editaron esto e hicieron correcciones o recomendaciones.

Además, gracias a Mathias Bynens por actualización de varias partes del artículo.