Desde hace algún tiempo, he estado leyendo sobre el patrón Promise y cómo nos permite mejorar el diseño de nuestro código JavaScript. Sin embargo, dicho patrón se asocia con frecuencia a peticiones asíncronas y en realidad son dos temas totalmente distintos.
Si bien es cierto que en HTML 5 existen los conocidos Web Workers, característica que nos permite lanzar procesos en hilos secundarios, Promise Pattern lo que nos proporciona es otra forma de estructurar nuestro código en cuanto a los callbacks se refiere.
Veamos todo esto con un ejemplo: Supongamos que tengo una función llamada doStuff donde sé que puedo obtener distintos comportamientos al respecto: el proceso ha terminado de manera satisfactoria, ha ocurrido un error, etcétera. La forma más habitual de gestionar este proceso sería del siguiente modo:
var result = doStuff();
console.log("The result: " + result);
El patrón Callback
En el ejemplo anterior, hacemos una llamada a nuestra función y guardamos el resultado de la misma una vez haya terminado. Con ese resultado, seríamos capaces de determinar si el proceso se ha ejecutado de manera satisfactoria o no.
Sin embargo, lo ideal (o uno de los acercamientos posibles) sería que mi propia función fuera lo suficientemente inteligente para saber a qué otra función tiene que llamar una vez que su tarea haya finalizado, o callback 🙂 Para ello podríamos modificar la función anterior de la siguiente manera:
function success(result) {
console.log("The result: " + result);
}
O lo que es lo mismo:
doStuff(function(result) {
console.log("The result: " + result);
});
De este modo, la función que pasamos como parámetro será la encargada de gestionar el resultado como tal, encapsulado de esta forma posibles variables necesarias para el proceso, así como otras funciones que pudiéramos crear dentro de este callback. De hecho, podemos pasar por parámetro tantas funciones como sean necesarias si existieran distintos resultados que necesitaran de distintos procesos:
doStuff(
function(result) {
console.log("The result: " + result);
},
function(error) {
console.log("Error: " + error);
}
);
Si dejamos de lado la forma de llamar a la función y nos centramos en la misma, esta podría ser su posible estructura:
function doStuff(successCallback, errorCallback) {
try { //do stuff
//...
if (typeof successCallback === "function")
successCallback("Your request has been successfully processed.");
} catch (ex) {
//Something bad happened
if (typeof errorCallback === "function")
errorCallback("Your request has failed.");
}
}
Esta solución, conocida como Callback Pattern podría mejorar nuestra arquitectura, pero también puede llegar a ser bastante tediosa si las funciones de callback, además, tienen llamadas a otras funciones que utilizan este mismo sistema. Sería muy complicado de seguir.
¿Qué es lo que ofrece el patrón promise?
Lo que nos facilita este patrón es una forma distinta de organizar esas llamadas de callback 🙂 Podemos leer más sobre las líneas generales de este patrón en Promises/A de CommonJS, donde nos dice que una promesa puede tener los siguientes estados: satisfecho, insatisfecho y fallido, traducido literalmente 🙂 La diferencia fundamental es que en el patrón Callback era necesario indicarle en la llamada cuál iba a ser nuestra función de vuelta, para cada uno de los casos, y en las librerías que implementan este patrón siguen una arquitectura más robusta, a la vez que sencilla, para manejar estos supuestos.
Veamos todo esto con un par de ejemplos algo más reales: Para poder hacer distintas pruebas he creado una pequeña aplicación que devuelve una serie Fibonacci que le solicitemos por pantalla. El ejemplo a través del patrón callback podría ser el siguiente:
function calculateFibonacci(number) {
if (number == 0 || number == 1)
return number;
return (calculateFibonacci(number - 1) + calculateFibonacci(number - 2));
}
function doStuff(serie, successCallback, errorCallback) {
try {
var results = [];
for (var i = 0; i < serie - 1; i++) {
var result = calculateFibonacci(i);
console.log(result);
results.push(result);
}
console.log("for finished");
if (typeof successCallback === "function") {
successCallback(results.join(","));
}
} catch (ex) {
if (typeof errorCallback === "function") {
errorCallback(ex.message);
}
}
}
function output(log, msg) {
log.innerHTML += "<li>" + msg + "</li>";
}
window.onload = function() {
var log = document.getElementById("log");
var btnGetSerie = document.getElementById("btnGetSerie");
btnGetSerie.addEventListener("click", function() {
output(log, "Initialization");
var value = document.getElementById("serie").value;
doStuff(value, function(result) {
output(log, "This is your result: " + result);
}, function(error) {
output(log, "Something bad happened: " + error);
});
output(log, "doStuff has already called");
});
};
Si nos fijamos en el código anterior, he creado mi función doStuff que, a su vez, llama a calculateFibonacci el número de veces que le haya llegado como parámetro. Además, recibe dos funciones successCallback y errorCallback. Dependiendo de cómo trascurra el proceso definido dentro de doStuff se llamará a una de las dos. La primera de ella será ejecutada, previa confirmación de que es una función, una vez tengamos todos los resultados de la serie. La segunda de ellas solamente será invocada si mi código lanza una excepción y por lo tanto entra en el apartado del catch. Para que esas funciones sean llamadas por mi código, mi función debe ser consciente de ellas y ejecutarlas bajo el nombre que se les ha asignado como parámetro.
Por último, si nos fijamos en la forma de invocar a la función sería pasándole como primer parámetro el valor utilizado para genera la serie, la función que se lanzará en caso de éxito, directamente en la llamada como una función anónima, y la función en el caso de error.
En el caso de trabajar con el patrón Promise, el ejemplo varía de la siguiente manera:
function calculateFibonacci(number) {
if (number == 0 || number == 1)
return number;
return (calculateFibonacci(number - 1) + calculateFibonacci(number - 2));
}
function doStuff(serie) {
var deferred = $.Deferred();
try {
var results = [];
for (var i = 0; i < serie - 1; i++) {
var result = calculateFibonacci(i);
console.log(result);
results.push(result);
}
console.log("for finished");
deferred.resolve(results.join(","));
} catch (ex) {
deferred.reject(ex.message);
}
return deferred.promise();
}
function output(log, msg) {
log.innerHTML += "<li>" + msg + "</li>";
}
window.onload = function() {
var log = document.getElementById("log");
var btnGetSerie = document.getElementById("btnGetSerie");
btnGetSerie.addEventListener("click", function() {
output(log, "Initialization");
var value = document.getElementById("serie").value;
var promise = doStuff(value);
promise.then(function(result) {
output(log, "This is your result: " + result);
}, function(error) {
output(log, "Something bad happened: " + error);
});
output(log, "doStuff has already called");
});
};
En este ejemplo estoy utilizando la libería JQuery y su objeto $.Deferred, incluido en la misma desde la versión 1.5. Antes de ejecutar el código de nuestra función doStuff, creamos un objeto de dicho tipo y ejecutamos revolve o reject en aquellos supuestos donde con callback llamábamos a nuestra funciones pasadas como parámetro. Por último, devolvemos la referencia a promise, objeto que sólo expone aquellos métodos necesarios para añadir los manejadores adicionales: then, done, fail, notify, resolveWith, rejectWith y notify with. Más información.
Desde el lado de la llamada, recuperaremos dicho objeto y asignaremos el manejador oportuno, en este caso then, el cual me permite pasarle como parámetros la función de success y error, pero mi función como tal no tendrá noción de las mismas sino que es en este caso JQuery quien se encarga de gestionarlo. Mi función sólo deberá indicar dentro de su proceso en qué casos se considera que dicha promise queda resuelta o no a través de los métodos mencionados anteriormente.
Actualización 27/01/2019: Ahora, con ECMAScript 6 ya no es necesario utilizar librerías externas para gestionar promesas. Lee este artículo para saber más.
Promise y funciones asíncronas
En muchos artículos que he leído, se relaciona a el patrón promise con peticiones asíncronas. Si bien son dos escenarios que funcionan muy bien juntos, uno no asegura el otro. Para poder comprobar que efectivamente su ejecución no es asíncrona es posible comprobarlo en el ejemplo anterior donde lanzo distintos mensajes al usuario. Si lanzamos el ejemplo comprobamos que el mensaje doStuff has already called siempre se muestra en el último lugar, ya sea con más o menos valores en la serie (tarde más o menos). Por lo que la asincronía no depende de Promise sino de lo que se haya lanzando en la función que la contiene.
Hasta la aparición de Web Workers en HTML 5 los desarrolladores no tenían la posibilidad de lanzar distintos hilos para la ejecución de sus procesos, toda la aplicación utilizaba un único hilo. Lo que si es cierto es que existían distintas técnicas para simular un comportamiento similar, aunque no dejaba de ser bloqueante. Haciendo uso de setTimeout y setInterval podemos crear la ilusión de que estamos ejecutando varias tareas al mismo tiempo pero, en realidad dejamos en suspensión unas u otras durante un lapso de tiempo, permitiendo que mientras tanto se ejecuten las siguientes, como haría la CPU cuando gestiona sus procesos. He modificado el ejemplo del patrón callback anterior para comprobar que efectivamente en el tiempo de retardo del timeout que encapsula el comportamiento de doStuff, se permite continuar con el código que aparece a continuación, en este caso output(log, «doStuff has already called»);
function calculateFibonacci(number) {
if (number == 0 || number == 1)
return number;
return (calculateFibonacci(number - 1) + calculateFibonacci(number - 2));
}
function doStuff(serie, successCallback, errorCallback) {
setTimeout(function() {
try {
var results = [];
for (var i = 0; i < serie - 1; i++) {
var result = calculateFibonacci(i);
console.log(result);
results.push(result);
}
console.log("for finished");
if (typeof successCallback === "function") { successCallback(results.join(",")); }
} catch (ex) {
if (typeof errorCallback === "function") {
errorCallback(ex.message);
}
}
}, 0);
}
function output(log, msg) {
log.innerHTML += "<li>" + msg + "</li>";
}
window.onload = function() {
var log = document.getElementById("log");
var btnGetSerie = document.getElementById("btnGetSerie");
btnGetSerie.addEventListener("click", function() {
output(log, "Initialization");
var value = document.getElementById("serie").value;
doStuff(value,
function(result) {
output(log, "This is your result: " + result);
},
function(error) {
output(log, "Something bad happened: " + error);
});
output(log, "doStuff has already called");
});
};
Este es un ejemplo bastante simplista pero podéis encontrar librerías como async.js para este tipo de implementaciones.
Como conclusión, el patrón Promise es realmente interesante y nos permite despreocuparnos de parte de la gestión que era necesaria con el patrón callback, pero no trata de peticiones asíncronas. Si que es cierto que a día de hoy está teniendo muy buena acogida en distintas tecnologías como JQuery, Dojo, aplicaciones con HTML 5 y las aplicaciones Windows 8 para desarrolladores web y puede permitir que nuestro código sea más llevadero.
En los siguiente enlaces podemos probar los tres ejemplos anteriores:
JavaScript Callback Pattern
Promise Pattern – JQuery
JavaScript Callback Pattern – setTimeout
¡Saludos!