Entendiendo las promesas y su importancia
Hay algo fundamental para poder entender las promesas y la revolución que suponen. JavaScript es de un solo hilo, es decir, dos porciones de secuencia de comandos no se pueden ejecutar al mismo tiempo, tienen que ejecutarse uno después del otro. En navegadores, JavaScript comparte un hilo con una carga de otras cosas que difiere de navegador en navegador. Pero, generalmente, JavaScript se encuentra en la misma cola que la pintura, la actualización de estilos y el control de acciones de usuario (como destacar texto e interactuar con controles de formulario). La actividad en uno de estos elementos retarda a los otros.
Para evitar eso, hasta ahora se han usado eventos y callbacks.
Por ejemplo:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// imagen cargada
});
img1.addEventListener('error', function() {
// algo salió mal
});
Por desgracia, en el ejemplo anterior, es posible que los eventos ocurran antes de que comencemos a escucharlos. Por eso, debemos solucionar este problema usando la propiedad “complete” de las imágenes:
var img1 = document.querySelector('.img-1');
function loaded() {
// imagen cargada
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// algo salió mal
});
Esto no captura imágenes que generaron n error antes de que pudiéramos escucharlas. Lamentablemente, el DOM no nos brinda una forma de hacerlo. Además, en este ejemplo, solo intentamos cargar una imagen. La complejidad aumenta aún más cuando deseamos saber cuándo se cargó un conjunto de imágenes.
Los eventos son excelentes para cosas que pueden suceder varias veces en el mismo objeto, porque en ese caso no interesa saber realmente lo que ocurrió antes de adjuntar el receptor. Pero si se trata de éxito/fallo asincrónico, idealmente, querrás algo así:
img1.callThisIfLoadedOrWhenLoaded(function() {
// cargada
}).orIfFailedCallThis(function() {
// fallo
});
// y...
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// todo cargado
}).orIfSomeFailedCallThis(function() {
// uno o más fallos
});
Las promesas hacen eso, aunque con una mejor nomenclatura. Si los elementos de imagen HTML tuviesen un método "ready" que mostrara una promesa, podríamos hacer lo siguiente:
img1.ready().then(function() {
// éxito
}, function() {
// fallo
});
// y…
Promise.all([img1.ready(), img2.ready()]).then(function() {
// todo bien
}, function() {
// al menos un fallo
});
Fundamentalmente, las promesas se parecen un poco a los receptores de eventos, a excepción de lo siguiente:
- Una promesa solo puede completarse con éxito o fallar una vez. No puede completarse con éxito o fallar dos veces, ni puede pasar de exitoso a fallido ni viceversa.
- Si una promesa se ha completado con éxito o ha fallado y luego agregas un callback de exitoso/fallido, se llamará al callback correcto, a pesar de que el evento haya sucedido antes.
Esto es extremadamente útil para el éxito o fracaso de procesos asincrónicos porque es menos importante el momento exacto de la disponibilidad que la reacción ante el resultado.
Entonces... ¿qué son las promesas?
Son una API que nos ayudará a realizar cosas antes complicadas o imposibles debido a lo que se ha dicho más arriba.
La imagen nos muestra el ciclo de vida y el funcionamiento de una promesa. Como prometido, he traducido las explicaciones de la imagen :)
Una promesa puede ser de estas clases:
fulfilled
(cumplida): la acción relacionada con la promesa se completa con éxito.
rejected
(rechazada): la acción relacionada con la promesa no se completa con éxito.
pending
(pendiente): aún no se completa ni se rechaza.
settled
(finalizada): se completa o se rechaza.
En las especificaciones, también aparece el término thenable
para describir un objeto parecido a una promesa porque tiene un método then
.
Cabe decir que hace tiempo que las promesas existen en forma de bibliotecas. Las siguientes son algunas:
La revolución de las promesas empezó a través de librerías como estas y de frameworks que las usan como medio primario para manipular la asincronía en su código. Desde el 2013, las promesas están disponibles de manera nativa en los exploradores modernos, lo cual será decisivo en el futuro.
Estas bibliotecas y las promesas de JavaScript tienen en común un comportamiento estandarizado llamado Promises/A+. Si usas jQuery, encontrarás algo similar llamado Deferred. Sin embargo, Deferred no cumple con Promise/A+, por lo cual es un tanto diferente y menos útil, así que ten cuidado. jQuery también tiene un tipo Promise, pero solo se trata de un subconjunto de Deferred y no funciona muy bien.
Si bien las implementaciones de las promesas cumplen con un comportamiento estandarizado, las API generales son diferentes. Las API de las promesas de JavaScript son similares a las de RSVP.js.
Las promesas de JavaScript empezaron en DOM como “Future”, se les cambió el nombre a “Promise” y, finalmente, se trasladaron a JavaScript. Es fabuloso contar con ellas en lugar del DOM en JavaScript porque estarán disponibles en contextos de JS sin navegador, como Node.js.
Si bien son una funcionalidad de JavaScript, el DOM las usa sin problemas cuando las necesita. De hecho, todas las nuevas API de DOM con métodos de éxito o falla asincrónicos usan promesas.
Viendo una promesa por dentro
Una promesa se crea así:
var promise = new Promise(function(resolve, reject) {
// hacer algo que puede ser asíncrono, then…
if (/* todo está bien */) {
resolve("Exito");
}
else {
reject(Error("Algo falló"));
}
});
El constructor de la promesa recibe un argumento: un callback con dos parámetros (resolve y reject). A continuación, se hace algo con el callback (tal vez un proceso asincrónico) y se llama a resolve si todo funciona bien o a reject si esto no sucede.
Como en throw del JavaScript que todos conocemos, es costumbre (aunque no obligación) aplicar reject con un objeto Error. La ventaja de los objetos Error es que capturan un seguimiento de pila; de esta forma, las herramientas de depuración son más útiles.
Para usar esta promesa:
promise.then(function(result) {
console.log(result); // "Todo bien!"
}, function(err) {
console.log(err); // Error: "Hubo un fallo"
});
then()
recibe dos argumentos: un callback para cuando se tiene éxito y otro para cuando sucede lo contrario. Ambos son opcionales; puedes agregar un callback solo para cuando se tiene éxito o se produce una falla.
Uso básico de las Promesas
Pienso esta parte como una especie de Promise by the example, para mostrar algunos casos y ejemplos de uso de las promesas.
El constructor new Promise()
sólo debe utilizarse para tareas asíncronas heredadas, como el uso de setTimeout
o XMLHttpRequest
. Se crea una nueva promesa con la nueva palabra clave y la promesa proporciona funciones de resolución y rechazo a la devolución de llamada proporcionada:
var p = new Promise(function(resolve, reject) {
// Hacer tarea asíncrona y then...
if(/* éxito */) {
resolve('Success!');
}
else {
reject('Fallo!');
}
});
p.then(function() {
/* hacer algo con el resultado */
}).catch(function() {
/* error :( */
})
Corresponde al desarrollador llamar manualmente a resolve
o reject
dentro del cuerpo de la devolución de llamada en función del resultado de su tarea. Un ejemplo realista sería convertir XMLHttpRequest
a una tarea basada en la promesa:
function get(url) {
// Devolver una nueva promesa.
return new Promise(function(resolve, reject) {
// Haz lo habitual de XHR
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// Esto es llamado incluso en error 404, etc
// entonces chequea el status
if (req.status == 200) {
// Resuelve la promesa con la respuesta
resolve(req.response);
}
else {
// o bien recahaza con el texto del status
reject(Error(req.statusText));
}
};
// Manejar errores de red
req.onerror = function() {
reject(Error("Error de Red"));
};
// Make the request
req.send();
});
}
// Use esto!
get('story.json').then(function(response) {
console.log("Éxito!", response);
}, function(error) {
console.error("Fallo!", error);
});
A veces no es necesario completar tareas asíncronas dentro de la promesa, si es posible que se tome una acción asíncrona, sin embargo, devolver una promesa será lo mejor para que siempre pueda contar con una promesa que sale de una función dada . En ese caso, simplemente puede llamar a promise.resolve()
o promise.reject()
sin usar la nueva palabra clave. Por ejemplo:
var userCache = {};
function getUserDetail(username) {
// En ambos casos, en caché o no, se devolverá una promesa
if (userCache[username]) {
// Retorna una promise sin la palabra clave "new"
return Promise.resolve(userCache[username]);
}
// Use la API fetch API para obtener información
// fetch devuelve una promise
return fetch('users/' + username + '.json')
.then(function(result) {
userCache[username] = result;
return result;
})
.catch(function() {
throw new Error('Usuario no encontrado: ' + username);
});
}
Puesto que siempre se devuelve una promesa, siempre puede usar los métodos then
y catch
en su valor de retorno.
then
Todas las instancias de Promise tienen un método then
que nos permite reaccionar a la promesa. El primer método de devolución de llamada recibe el resultado dado por la llamada resolve()
:
new Promise(function(resolve, reject) {
// Una acción asíncrona usando `setTimeout`
setTimeout(function() { resolve(10); }, 3000);
})
.then(function(result) {
console.log(result);
});
// En la consola:
// 10
La llamada de retorno se activa cuando se resuelve la promesa. También puede encadenar las devoluciones de llamada del método:
new Promise(function(resolve, reject) {
// Una tarea ansíncrona usando setTimeout
setTimeout(function() { resolve(10); }, 3000);
})
.then(function(num) { console.log('first then: ', num); return num * 2; })
.then(function(num) { console.log('second then: ', num); return num * 2; })
.then(function(num) { console.log('last then: ', num);});
// En la consola:
// first then: 10
// second then: 20
// last then: 40
Cada then
recibe el resultado del valor de retorno anterior.
Si una promesa ya se ha resuelto pero se vuelve a llamar, la devolución de llamada se disparará inmediatamente. Si la promesa es rechazada y usted llama entonces después del rechazo, el callback nunca se llama.
El callback catch
se ejecuta cuando se rechaza la promesa:
new Promise(function(resolve, reject) {
// Una tarea asíncrona usando setTimeout
setTimeout(function() { reject('Done!'); }, 3000);
})
.then(function(e) { console.log('done', e); })
.catch(function(e) { console.log('catch: ', e); });
// From the console:
// 'catch: Done!'
Lo que usted proporcione al método de rechazo depende de usted. Un patrón frecuente es enviar un error a la captura:
reject(Error('Data could not be found'));
promise.all
Piense en los cargadores de JavaScript: hay momentos en los que se desencadenan múltiples interacciones asíncronas, pero sólo se quiere responder cuando se completan todos ellos - ahí es donde promise.all
entra en juego. El método promise.all
toma una serie de promesas y dispara una devolución de llamada una vez todos están resueltos:
Promise.all([promise1, promise2]).then(function(results) {
// Ambas promesas resultas
})
.catch(function(error) {
// Una o más promesas rechazadas
});
Una forma perfecta de pensar en Promise.all
es disparar múltiples solicitudes AJAX (via fetch) al mismo tiempo:
var request1 = fetch('/users.json');
var request2 = fetch('/articles.json');
Promise.all([request1, request2]).then(function(results) {
// Todas las promesas resueltas!
});
Podrías combinar APIs como fetch
y Battery API
, ya que ambas retornan promesa:
Promise.all([fetch('/users.json'), navigator.getBattery()]).then(function(results) {
// Todas las promesas resueltas!
});
Lidiar con el rechazo es, por supuesto, difícil. Si alguna promesa es rechazada el catch
es lanzado en el primer rechazo:
var req1 = new Promise(function(resolve, reject) {
// Una tarea asíncrona usando setTimeout
setTimeout(function() { resolve('First!'); }, 4000);
});
var req2 = new Promise(function(resolve, reject) {
// A mock async action using setTimeout
setTimeout(function() { reject('Second!'); }, 3000);
});
Promise.all([req1, req2]).then(function(results) {
console.log('Then: ', results);
}).catch(function(err) {
console.log('Catch: ', err);
});
// From the console:
// Catch: Second!
Promise.all
será super útil a medida que más APIs se mueven hacia promesas.
Promise.race
`Promise.race` es una función interesante - en lugar de esperar a que todas las promesas sean resueltas o rechazadas, `Promise.race` se activa tan pronto como se resuelve o rechaza cualquier promesa en la matriz:
var req1 = new Promise(function(resolve, reject) {
// Tarea asíncrona usando setTimeout
setTimeout(function() { resolve('First!'); }, 8000);
});
var req2 = new Promise(function(resolve, reject) {
// Tarea asíncrona usando setTimeout
setTimeout(function() { resolve('Second!'); }, 3000);
});
Promise.race([req1, req2]).then(function(one) {
console.log('Then: ', one);
}).catch(function(one, two) {
console.log('Catch: ', one);
});
// Consola
// Then: Second!
Un caso de uso podría estar provocando una solicitud a una fuente primaria y una fuente secundaria (en caso de que la primaria o la secundaria no estén disponibles).
Compatibilidad con navegadores y polyfill
En la actualidad, ya existen implementaciones de promesas en los navegadores.
A partir de Chrome 32, Opera 19, Firefox 29, Safari 8 y Microsoft Edge, las promesas vienen habilitadas de forma predeterminada.
Consulta el polyfill si deseas que los navegadores sin implementaciones completas de promesas cumplan con las especificaciones, o si quieres agregar promesas a otros navegadores y Node.js.
Compatibilidad con otras bibliotecas
La API de las promesas de JavaScript tratará a todos los elementos con un método then()
como si fueran promesas (o thenable, si se usa el idioma de las promesas). Por lo tanto, no habrá problema si usas una biblioteca que muestra promesas; funcionará bien con las nuevas promesas de JavaScript.
A pesar de que, como ha dicho, los Deferreds de jQuery son un poco inútiles. Afortunadamente, puedes transmitirlos a las promesas convencionales. Vale la pena hacerlo lo más pronto posible.
Ejemplo:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
En este caso, $.ajax
de jQuery muestra un elemento Deferred. Ya que tiene un método then(), Promise.resolve()
puede convertirlo en una promesa de JavaScript. Sin embargo, algunos deferreds pasan varios argumentos a sus callbacks, por ejemplo:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
En cambio, las promesas de JS ignoran todos menos el primero:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
Afortunadamente, esto suele ser lo que quieres o, al menos, te brinda acceso a lo que quieres. Además, ten en cuenta que jQuery no sigue la convención de pasar objetos Error a rechazos.
Conclusión
Las promesas han sido un tema candente para los últimos años, y han pasado de un patrón de framework de JavaScript a un elemento básico del idioma. Iremos viendo cómo la mayoría de las nuevas API JavaScript se implementarán con un patrón basado en la promesa ...
... y eso es una gran cosa! Gracias a las promesas, los desarrolladores serán capaces de evitar el infierno de devolución de llamada (callback) y las interacciones asíncronas pueden ser transmitidas como cualquier otra variable. Quizá tome un poco de tiempo acostumbrarse a usarlas, pero ya tenemos a mano las herramientas, pues son nativas en la mayoría de navegadores modernos. ¡Ahora es el tiempo de aprender a usarlas!
Enlaces