Patrón Promise: Implementación

Lo prometido es deuda (bien lo saben las funciones) y he encontrado un momento para empezar a explicar la forma en la que he implementado el patrón Promise en mi caso, no tiene porqué ser la mejor, pero cumple con su cometido.

NOTA: Me gustaría implementarla en Test Driven Development, pero ya es bastante para quien lee y para el que escribe seguir la implementación como para encima añadir TDD, pero no quisiera dejar de recomendarlo.

Primero: Funcionalidad básica

Lo que necesitamos de un objeto Promise es:

  • Crear instancias totalmente independientes
  • Añadirle callbacks que serán llamados cuando se cumpla la promesa
  • Notificarle cuando se ha cumplido la promesa

Con los objetivos en la mano es más sencillo ver que hacer, lo primero necesitamos una clase, a la hora de crear clases en Javascript yo me decanto por el patrón de constructor con prototipos que espero explicar algún día.

function Promise() { }

Segundo punto: poder añadirle callbacks, ésto consiste en el método .then() al que deberemos poder llamar pasándole las funciones que queremos que se ejecuten cuando la promesa se cumpla. Puesto de debe poderse añadir más de un callback para cada promise lo más lógico sería crear un Array donde almacenarlos

function Promise() {
  this._callbacks = [];
}

Y el método .then() que vaya añadiendo al Array los callbacks que se le pasen, puesto que es mejor que los errores se detecten cuanto antes también podemos asegurarnos que el callback es una función:

Promise.prototype.then = function(callback)  {
  if (typeof callback !== 'function') {
    throw new Error("[Promise.then] El argumento 'callback' no es una función " + typeof callback);
  }

  this._callbacks.push(callback);
};

Y ahora que ya tenemos todos los callbacks en un Array necesitamos algún sistema para avisarle al Promise que ya tiene los datos que necesita y que se los pase a los callbacks. Sobre ésto no he visto ninguna implementación, pero a mi me parece bastante razonable crear un método Promise.done() que notifica al Promise que ya está cumplido y ejecuta los callbacks.

Promise.prototype.done = function() {
  var callback;
  for (var i = 0; i < this._callbacks.length; i++) {
    callback = this._callbacks[i];
    callback();
  }
};

Y ya lo tenemos hecho, hemos creado un Promise básico, vamos a probarlo. Imaginemos cualquier función asíncrona, por ejemplo vamos a crear una función que nos avise cuando pase un segundo:

function esperarUnSegundo() {
  var promise = new Promise();
  // Hacemos un timeout a mil milisegundos
  setTimeout(function() {
    promise.done();
  }, 1000);
  return promise;
}

esperarUnSegundo().then(function() {
  alert("Ha pasado un segundo =D");
});

Pruébame

Si probamos todo el código veremos que al cabo de un segundo ejecuta el alert.

Todo funciona perfectamente, vamos un punto más allá, ésta vez descarguemos una página, como no nos importa ahora mismo el código que descarga la página fingiremos llamar a una función peticiónHttp(url, callback) que lo hará por nosotros.

function descargar(url) {
  var promise = new Promise();
  peticiónHttp(url, function(codigoHtml) {
    promise.done();
  });
  return promise;
}
descargar('www.google.com').then(function() {
  // Y ahora?
});

Sorpresa! La función ha descargado la página y obtenido el html, pero nuestro Promise no ha sido capaz de pasarlo al callback. La función del Promise en un principio era avisar cuando una tarea asíncrona termina, pero la mayoría de las tareas asíncronas devuelven un resultado y cuando avisemos al Promise que se ha cumplido también querremos que pase el resultado a todos los callbacks. Para ello modificaremos el método done y para que pase a los callbacks todos los argumentos que se le pasen a él (si no sabes lo que hace el método apply puedes mirarlo aquí):

Promise.prototype.done = function() {
  // Guardamos los argumentos que se le ha pasado a .done()
  var args = arguments;
  var callback;
  for (var i = 0; i < this._callbacks.length; i++) {
    callback = this._callbacks[i];
    // Y se los pasamos al callback
    callback.apply(null, args);
  }
};

Y ya está, ahora podemos pasarle argumentos a .done():

function descargar(url) {
  var promise = new Promise();
  peticiónHttp(url, function(codigoHtml) {
    promise.done(codigoHtml);
  });
  return promise;
}

descargar('www.google.com').then(function(codigoHtml) {
  alert(codigoHtml);
});

Ya tenemos nuestra versión 0.1 de la clase Promise 😀

Segundo: Gestión de errores

Hasta aquí ya tenemos un Promise con el que avisar cuando acaba una tarea asíncrona, pero nos olvidamos de algo muy importante, a la hora de programar no todo sale como quisiéramos y muchas veces nos encontramos con errores, que pasaría si peticiónHttp() fallara? Que jamás se ejecutaría el .done() del Promise que hemos devuelto y el callback esperará sentado a que lo llamen el resto de su vida. Hay que preparar el Promise para que avise cuando algo va mal. Necesitamos añadirle al Promise:

  • Poder añadir callbacks especiales para cuando se produzca un error
  • Avisarle cuando se produzca un error
  • Que le pase al callback de error el objeto Error que se ha lanzado

Lo primero es que el Promise no solo reciba un callback normal sino que también reciba otro callback que será ejecutado solo si se produce un error. Una idea que me gusta es dárselo al método .then() como segundo argumento, ya que el primero es el callback normal. Y éste debería guardarlo, para ello debemos crear otro Array donde guardar los callbacks de errores:

function Promise() {
  this._callbacks = [];
  this._onError = [];
}
Promise.prototype.then = function(callback, onError) {
  // Validamos el callback normal
  if (typeof callback !== 'function') {
    throw new Error("[Promise.then] El argumento 'callback' no es una función " + typeof callback);
  }
  // Validamos el callback de error. Como es opcional puede ser 'undefined' o una función
  if (onError && typeof onError !== 'function') {
    throw new Error("[Promise.then] El argumento 'onError' no es una función " + typeof onError);
  }

  this._callbacks.push(callback);
  // Si no era undefined debe ser una función, porque ya lo validamos
  if (onError) {
    this._onError.push(onError);
  }
};

Como se ve es prácticamente lo mismo que para los callbacks, ya que se trata de lo mismo, un callback por si hay errores. Ahora vamos a matar los últimos dos puntos de un tiro. Añadiremos un método para avisar al Promise cuando se produzca un error y le pasaremos el objeto Error para que lo pase a todos los callbacks de error.

Promise.prototype.fail = function(error) {
  var callback;
  for (var i = 0; i < this._onError.length; i++) {
    callback = this._onError[i];
    callback(error);
  }
};

Y ya está, ahora cuando llamemos al método .fail() llamará a todos los callbacks de error y les pasará el objeto Error. Ahora podemos adaptar la función descargar() para que también notifique cuando se produzca un error:

function descargar(url) {
  var promise = new Promise();
  try {
    peticiónHttp(url, function(codigoHtml) {
      promise.done(codigoHtml);
    })
  } catch (error) {
    promise.fail(error);
  }
  return promise;
}
descargar('www.google.com').then(function(codigoHtml) {
  alert(codigoHtml);
});

Ahora ya podemos decir que tenemos la versión 0.2 del Promise tengo que dejar para otro post métodos más complicados como .then() concatenados y el .and() porque ya es muy tarde. Aquí dejo el código completo al que le he añadido la propiedad _estado para evitar que se pueda cumplir o fallar un Promise cuando ya está cumplido o fallado.

function Promise() {
  this._callbacks = [];
  this._onError = [];
  this._estado = "esperando";
}

Promise.prototype.then = function(callback, onError) {
  // Validamos el callback normal
  if (typeof callback !== 'function')
    throw new Error("[Promise.then] El argumento 'callback' no es una función " + typeof callback);

  // Validamos el callback de error. Como es opcional puede ser 'undefined' o una función
  if (onError && typeof onError !== 'function')
    throw new Error("[Promise.then] El argumento 'onError' no es una función " + typeof onError);

  this._callbacks.push(callback);
  // Si no era undefined debe ser una función, porque ya lo validamos
  if (onError) 
    this._onError.push(onError);
};

Promise.prototype.done = function(error) {
  if (this._estado !== 'esperando')
    throw new Error('Intentando cumplir un promise que ya ha finalizado');

  this._estado = "cumplido";
  // Guardamos los argumentos que se le ha pasado a .done()
  var args = arguments;
  var callback;

  for (var i = 0; i < this._callbacks.length; i++) {
    callback = this._callbacks[i];
    // Y se los pasamos al callback
    callback.apply(null, args);
  }
};

Promise.prototype.fail = function(error) {
  if (this._estado !== 'esperando') throw new Error('Intentando hacer fallar un promise que ya ha finalizado');

  this._estado = "fallado";
  var callback;

  for (var i = 0; i < this._onError.length; i++) {
    callback = this._onError[i];
    callback(error);
  }
};

215 thoughts on “Patrón Promise: Implementación

  1. nesesito una ayuda y no sabia donde buscarla, pero por lo que veo tu sabes mucho acerca de esto..
    bueno es esto:
    como podria hacer ma o menos un metodo que reccorriera el documento y guardara todos los elementos que cumplan con una condicion en un array y despues llamar a una funcion por cada un de los elementos que cumplieron con la condicion(la funcion a llamar sera pasada por parametro)..
    yo se que esta pregunta no biene al caso en este tema de tu publicacion pero queria saber si tu me puedes ayudar…
    gracias de antemano

    1. Buenas Luis, en principio tu pregunta es muy genérica, no se a que te refieres con cumplir una condición. En principio en cualquier navegador moderno puedes utilizar document.querySelectorAll() para filtrar los elementos mediante un selector css por ejemplo:

      var elements = document.querySelectorAll('#sidebar .links > div[href~="wikipedia"]');

      Espero que te sirva de ayuda, en caso contrario te recomiendo plantear tu problema en StackOverflow 🙂

  2. De lo mejorcito que he encontrado en la web de este tema. Por fin vamos entendiendo los neófitos en esto del Javascript como funicionan las promises. Esperando ansioso la segunda parte.

    Muchas gracias.

  3. David Wagner’s Terriers followed up a 0-0 draw at champions Manchester City with a 1-1 draw at Chelsea on Wednesday night which confirmed their Premier League status for a second season. Huddersfield achieving Premier League survival is ‘unbelievable’, says Christopher Schindler: ‘Everybody else thought we were going down… it will take a few days for it to sink in’

  4. Hey just wanted to give you a quick heads up.
    The text in your post seem to be running off the screen in Ie.

    I’m not sure if this is a format issue or something to do with browser
    compatibility but I figured I’d post to let you know.

    The layout look great though! Hope you get the problem solved soon.
    Thanks

  5. I just couldn’t go away your web site before suggesting that I actually
    enjoyed the standard info a person supply in your guests? Is gonna be back incessantly to investigate cross-check new posts

  6. Hey there just wanted to give you a quick heads up.
    The words in your article seem to be running off the screen in Safari.
    I’m not sure if this is a formatting issue or something to do with
    browser compatibility but I thought I’d post to let you know.
    The design look great though! Hope you get the issue fixed soon.
    Cheers

  7. Thanks , I’ve recently been looking for information approximately this
    subject for a while and yours is the best I have came upon so far.
    But, what about the conclusion? Are you sure about the supply?

  8. Hi would you mind letting me know which hosting company
    you’re using? I’ve loaded your blog in 3 different browsers and I must say this blog loads a lot faster then most.
    Can you suggest a good hosting provider at a honest price?
    Kudos, I appreciate it!

  9. It is appropriate time to make a few plans for the long run and it’s time to be happy.

    I have read this submit and if I may just I desire to suggest
    you few attention-grabbing issues or tips. Maybe you could write subsequent articles relating to this article.
    I desire to read even more issues about it!

  10. I assume this is one of one of thhe most regressive moves in the background of Wikkipedia – by putting inn a
    login ‘barrier’ i would presume a great deal oof the casual (however as yoou explained,
    really critical) payment is avoided.

  11. It’s the best time to make some plans for the future and it is time to be happy.
    I have read this post and if I could I wish to suggest you some interesting things or tips.
    Maybe you could write next articles referring to this article.

    I wish to read even more things about it!

  12. I’ve been browsing online more than 2 hours today, yet I never
    found any interesting article like yours. It is pretty worth enough
    for me. In my view, if all web owners and bloggers made good content as you did, the
    web will be much more useful than ever before.

  13. Greetings from Los angeles! I’m bored to tears at work so I decided to
    browse your website on my iphone during lunch break.
    I really like the information you provide here and can’t wait to take a look when I get home.
    I’m surprised at how quick your blog loaded on my phone ..
    I’m not even using WIFI, just 3G .. Anyways, wonderful blog!

  14. Thanks a lot for sharing this with all folks you really recognize what you are talking approximately! Bookmarked. Kindly additionally consult with my web site =). We could have a hyperlink trade arrangement between us!

  15. I simply want to mention I’m beginner to weblog and actually savored this page. Almost certainly I’m likely to bookmark your blog . You absolutely have perfect well written articles. Regards for revealing your blog site.

  16. Kurye olarak hizmet veren kişiler, bulundukları lokasyondaki her adrese en kısa sürede ulaşma yolunu bilirler. Üstelik verilen bu görevi hava, trafik ve buna benzer diğer olumsuz şartlardan etkilenmeden yerine getirirler. Yani iş hayatı başta olmak üzere hayatın hemen her alanında önemli görevleri üstlenebilir ve başarıyla yerine getirebilirler. Özellikle büyük şehirlerdeki trafik kaosu ve adres konusundaki karmaşa göze alınırsa, kurye hizmetlerinden faydalanmanın bazı durumlarda zorunlu hale geleceği anlaşılabilir. Örneğin İstanbul’un bir noktasından diğer bir noktasına acil şekilde önemli bir evrak ulaştırmak gerektiğinde ya da hastanızın ihtiyacı olan ilacı acil ve güvenli bir şekilde ulaştırmak gerektiğinde, İstanbul moto kurye hizmetimizden faydalanarak sorununuza çözüm üretebilirsiniz.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *