sábado, 25 de abril de 2020

Node.js 14 - Características nuevas

Node 14 acaba de ser lanzado (hace unos pocos días cuando se escribe el post) y tiene un par de características interesantes que quisiera compartir con ustedes (solo un resumen, puedes ver todas las características nuevas en https://nodejs.org/en/blog/release/v14.0.0/). Al grano!!!

1. Optional Chaining (Operador ?)

Era bastante moroso (tedioso, molesto, aburrido, etc.) estar validando una serie de propiedades de un objeto para estar seguros que existían.

Primero, el error común:

const obj = {
    atrib: {
        a: 1
    },
    otroAtrib: {
        a: 1
    }
};

// imprime 1 ya que la propiedad existe
console.log(obj.atrib.a);

// Ups, cannot read property "a" of undefined (error, tristes noticias)
console.log(obj.atribNoExiste.a);

Ahora la solución habitual (condiciones, condiciones, CONDICIONES!!!):

const obj = {
    atrib: {
        a: 1
    },
    otroAtrib: {
        a: 1
    }
};

if (obj !== undefined 
   && obj.atribNoExiste !== undefined
   && obj.atribNoExiste.a !== undefined) {
    console.log(obj.atribNoExiste.a);
} else {
    console.log("La propiedad necesaria no existe");
}

Y ahora un poco de magia...

const obj = {
    atrib: {
        a: 1
    },
    otroAtrib: {
        a: 1
    }
};

// Maravilla!!! no sería error, retornaría undefined
console.log(obj.atribNoExiste?.a);

console.log(obj.atribNoExiste?.otro.otro);

console.log(obj.otroAtrib?.b?.d);

El operador ? nos ayudará a evitar las condiciones del mal xD.

2. Null Coalescing (Operador ??)

La solución al problema de los valores nulos.

Primero, lo que se hacía comúnmente y que ocasionaba errores que encontré en muchos códigos fuente (el operador || suele utilizarse para dar valores "por defecto"):

function sumaCuatro(val) {
    // si val no existe, asignar el valor 10 (¿o no?)
    const num = val || 10;

    return num + 4;
}

// no mandamos ningún valor, el resultado sería 14
console.log(sumaCuatro());

// mandamos undefined, el resultado sería 14
console.log(sumaCuatro(undefined));

// mandamos 0, el resultado debería ser 4 pero será 14 :(
console.log(sumaCuatro(0));

El problema es que el operador || trabaja con valores falsy (https://developer.mozilla.org/en-US/docs/Glossary/Falsy), por lo tanto no solamente se asigna el valor por defecto cuando es null o undefined sino también cuando otro valor falsy es enviado (por ejemplo 0).

La solución (el esperado operador ??):

function sumaCuatro(val) {
    // si val no existe, asiganar el valor 10 (¿o no?)
    const num = val ?? 10;

    return num + 4;
}

// no mandamos ningún valor, el resultado sería 14
console.log(sumaCuatro());

// mandamos undefined, el resultado sería 14
console.log(sumaCuatro(undefined));

// mandamos 0, el resultado es 4 como debe ser!!!
console.log(sumaCuatro(0));

El operador ?? solamente asigna el valor por defecto en caso de que el valor comparado sea null o undefined.


3. Diagnostics

Se agregaron varias herramientas para diagnosticar tu aplicación en la versión 12 y ahora por fin son oficiales!!! Escribiré sobre estas herramientas en otro post porque es un poco más complejo y este es un post casual XD keep coding!!!



martes, 11 de abril de 2017

Cacheando contenido para hacer más rápido tu Sitio Web (Varnish)

Hola de nuevo, ahoa escribo para contarles algunos conceptos sobre la cache, para luego pasar a instalar y configurar Varnish, una herramienta especializada en el manejo de la cache. Así que vamos adelante, primero veamos algunos conceptos esenciales para el manejo de la cache.

Características del protocolo HTTP

Un recurso es el objetivo de un request con el protocolo HTTP, cada recurso es identificado por un URI (Uniform Resource Identifier).

En cuanto al manejo de la cache, las propiedades importantes del protocolo http son:
- stateless: que significa que cada request se puede entender por si solo, y que ademas no puede realizar ninguna transformación en otros requests que se estan realizando. Es como si cada request se considerara único.
- Idealmente, la conexión con el servidor es persistente (excepto con HTTP/1.0), para ello existe un header extra llamado Keep-Alive. Este header persiste la conexión entre el cliente y el servidor, lo que implica que no se esta iniciando de nuevo la conexión en cada solicitud, sino que se esta reusando una anterior, esto porque las conexiones son costosas.

- safe: un request se considera seguro si no produce ningún cambio en el servidor. Estos métodos son por ejemplo GET, HEAD, TRACE, OPTIONS, etc.

- idempotent: un request es idempotente si al repetirse con los mismo parametros el resultado es siempre el mismo, por ejemplo PUT, HEAD, DELETE (además, si es safe es idempotente). En pocas palabras, si se realiza el mismo request una y otra vez con los mismos parámetros el estado del servidor es el mismo, aunque la respuesta devuelta podria ser distinta. Por ejemplo, en una Api REST, si hacemos DELETE a un recurso la primera vez obtenemos un código 200 indicando que la eliminación fue exitosa, sin embargo la siguiente que lanzamos el request obtenemos un 404 porque el recurso no existe ya; si bien las respuestas fueron distintas, el estado del servidor es el mismo, la eliminación de un recurso.

Tomando en cuenta estas características, son canditatos para realizar la cache los métodos GET y HEAD. Adicionalmente se puede habilitar la cache para los demás métodos pero esto debe considerarse muy bien pues pueden obtenerse resultados no esperados.

¿Cómo controlo la cache?

La cache es controlada a traves de headers en la respuesta del backend (también se puede realizar manualmente a través de la consola administrativa). Los headers más importantes para el cache son:
- Age: este header es agregado por Varnish para indicarnos el tiempo que ha transcurrido desde que se realizo el pedido del recurso al backend. Sirve para calcular si un objeto en la cache esta fresco o debe ser actualizado comunicandose al backend nuevamente.

- Cache-Control: este encabezado especifica directivas que deben ser cumplidas por las caches. Puede tener varios atributos como valor, pero los más comunes son public que indica que cualquier cache puede almacenar este valor y max-age que indica el periodo en segundos durante el cuál la cache se considera fresca.
Ejemplo, puede ser almacenado por cualquier cache y su tiempo de validez es 20 segundos:
Cache-Control: public, max-age: 20

Este header es muy útil cuando utilizamos varnish para darle un tiempo de vida a los recursos almacenados en cache, sin embargo solamente es útil enlas respuestas del backend ya que Varnish no toma en cuenta este header en las solicitudes del cliente.

- Etag: Se utiliza para diferenciar los estados de la representación de un recurso. Básicamente es para ver cuando un recurso es diferente de otro.

- Expires: También se utiliza para definir el tiempo que un objeto en cache es fresco, sin embargo, para Varnish se utiliza primero siempre el header Cache-Control y luego, solo en caso de no encontrarse definido Cache-Control, se utliza Expires.

¿Com puedo diferencia un request y un response?

La primera línea de un request se denomina request-line, mientras que la primera línea de una respuesta se denomina status-line.

El request-line comienza con un método (GET, POST, ...) seguido por el recurso solicitado y la versión del protocolo (GET /ruta HTTP/1.1).

El status-line comienza con el protocolo seguido por el código y una frase del código (HTTP/1.1 200 Ok)

Ejemplo de Request:
GET         /resources/images    HTTP/1.1
      X-Real-IP: 192.118.21.65
      X-Forwarded-For: 192.118.21.65
      X-Forwarded-Proto: http
      Host: 192.118.2.5:80
      Connection: keep-alive
      Cache-Control:  public, max-age=60
      User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36
      Accept: */*
      Accept-Encoding: gzip, deflate, sdch
      Accept-Language: es-419,es;q=0.8,en;q=0.6
      Cache-Control:  public, max-age=60

Ejemplo de Response
HTTP/1.1     200     OK
     Content-Type: application/json; charset=utf-8
     Content-Length: 289
     Status: 200 OK
     X-Powered-By: Express
     Access-Control-Allow-Headers: Content-type,Accept,X-Access-Token,X-Key
     Access-Control-Allow-Origin: *
     Access-Control-Allow-Methods: GET,PUT,POST,DELETE,OPTIONS
     Cache-Control: public, max-age=30
     Date: Tue, 11 Apr 2017 21:51:48 GMT
     ETag: W/"121-d539f4ff"
     X-Powered-By: Phusion Passenger 5.1.2
     Server: nginx/1.10.2 + Phusion Passenger 5.1.2
     X-Varnish: 98316
     Age: 0
     Via: 1.1 varnish (Varnish/5.1)


¿Cuándo se considera fresco un recurso en la cache?

Un recurso se considera frescon en la cache mientras su tiempo de vida (ttl) sea mayor a su edad (age). Por ejemplo, si almacenamos un objeto en la cache con el header Cache-Control: public, max-age: 20, entonces el objeto se considera fresco por 20 segundos desde el momento en que se obtiene el registro del servidor backend. Un ejemplo bastante simplista de como funciona la cache:

  1. Al primer request (segundo 0), se verifica la cache, como no se encuentra se solicita el recurso al backend y este almacena en cache (validez de 20 segundos establecida por el header Cache-Control) para devolverse luego al cliente.
  2. El siguiente request (segundo 10), verifica en la cache, dado que el registro ya se encuentra cacheado (HIT) simplemente se devuelve al cliente sin consultar al backend nuevamente, además se puede observar un header Age con el valor 10 que indica que el recurso esta en cache por diez segundos.
  3. El siguiente request llega al segundo 31, se verifica la cache y el registro existe, sin embargo no es fresco, por lo cuál se vuelve a solicitar el recurso al backend, se almacena la respuesta en la cache y de devuelve la misma al cliente.

¿Cómo se calcula el tiempo de vida de un recurso en Varnish?

Para calcular el tiempo de vida en cache de un recurso utilizamos el parámetro ttl (time to live), que se inicializa con el primer valor que encuentra de los siguientes:
- s-maxage en el campo Cache-Control de los encabezados de la respuesta
- max-age en campo Cache-Control de los encabezados de la respuesta
- el valor del encabezado Expires en la respuesta
- el valor por defecto del ttl (se configura al iniciar el servicio de varnish con -t, por defecto es 120 segundos)

Adicionalmente, se puede dar un tiempo de gracia a los recursos, esto implica que, aunque su ttl haya expirado, todavía se mantiene el recurso en la cache por un tiempo determinado. Su valor por defecto es diez segundos. De esta manera, incluso si el servidor backend estuviera fuera de línea, varnish esta permitido de devolver el recurso que se encuentra en la cache por un tiempo "extra".

En caso de que el ttl haya expirado y se tenga configurado el tiempo de gracia, el servidor puede devolver un recurso que no este fresco y realizar una llamada al backend para refrescarlo luego asincronamente. Este concepto se explicará en futuros posts pero es necesario tomarlo en cuenta al momento de aprender los conceptos necesarios para entender la cache.

Estados de los recursos en la cache (https://varnish-cache.org)
Y eso es todo por ahora, keep caching :D

domingo, 26 de marzo de 2017

¿Cómo instalar y utilizar npm?

Hellooooo :D Es hora de aprender, primero a instalar npm y despues la forma de utilizar npm en el proceso de desarrollo para ayudarnos con tareas que necesitamos realizar recurrentemente.

Primero instalaremos npm:

En windows:

  1. Descargar el instalador de https://nodejs.org/en/download/
  2. Instalar :D
En ubuntu (Linux en general):
  1. Instalamos: $ sudo apt-get install npm
  2. Instalamos n para el manejo de versiones: $ sudo npm install -g n
  3. Ahora utilizaremos n para descargar la última versión de nodejs: $ sudo n latest
Para ver la versión que tenemos instalada, en la consola debemos realizar:

$ npm --version

Ahora a comenzar un proyecto (si no utilizamos -y es necesario responder a algunas preguntas al hacer npm init):

$ npm init -y

Este comando creará un archivo llamado package.json en la raíz del proyecto, con los siguientes campos:

  • name: con el nombre del proyecto
  • version: con la versión del proyecto
  • description: una descripción pequeña del proyecto
  • main: el script principal del proyecto, usualmente index.js
  • scripts: donde podemos almacenar comandos utilizados recurrentemente en el proyecto
  • keywords: algunas palabras clave para referirse al proyecto
  • author: el autor del proyecto
  • license: la licencia del proyecto

Para instalar modulos en nuestro proyecto utilizamos npm install, ahora instalaremos un pequeño servidor de pruebas para desarrollo:

$ npm install live-server --save-dev

Con este comando estamos indicando que queremos que se instale el módulo live-server como una dependencia de desarrollo. Revisando el archivo package json ahora existe un nuevo campo:

"devDependencies": {
    "live-server": "^1.2.0"
  }

Esto nos indica que las una dependencia para el ambiente de desarrollo se llama live-server.
Ahora instalaremos una dependencia de producción que se utiliza bastante:

$ npm install request --save

  "devDependencies": {
    "live-server": "^1.2.0"
  },
  "dependencies": {
    "request": "^2.81.0"
  }

Ahora se ha creado un nuevo campo llamado dependencies, ahí se almacenó la dependencia instalada como una dependencia de producción.

Entonces, existen dos nuevos campos en el package,json que son muuuuy (muuuy :D) útiles:

  • dev-dependencies: que son dependencias que se utilizan solamente en ambientes de desarrollo, como librerias de testing (mocha, tape, etc.), aserciones (chai), transpilers (babel) u otros.
  • dependencies: son las dependencias que son necesarias para el proyecto en el ambiente de producción.

Además, es necesario ver las dependencias que estan desactualizadas en nuestro proyecto, para eso utilizamos:

$ npm outdated

Ejemplo de comando outdated
Como podemos observar, el comando nos muestra la versión actual, la esperada y a última. Idealmente me gusta realizar este proceso dependencia por dependencia para verificar que todo esta corriendo como debe ser.

Sin embargo, también se puede actualizar todo sin necesidad de hacer el proceso manualmente, para ello:

$ npm update --save-dev --save

Tambien podriamos actulizar solamente un paquete y no todos, por ejemplo para actuaizar un paquete con el nombre underscore:

$ npm update underscore

Con esto se actualiza tanto el paquete dentro de la carpeta node_modules como las versiones en el archivo package,json.

También podemos installar paquetes globalmente, para ello se utiliza -g, por ejemplo:

$ npm install mocha -g

Ahora, podemos revisar cuales son los paquetes que se tienen instalados:

$ npm list

Listado de paquetes utilizando npm list


Crear scripts Propios

También podemos crear nuestros propios scripts para iniciar, por ejemplo, para iniciar live server se debe hacer:

$ ./node_modules/.bin/live-server

Pero esta tarea debe realizarse varias veces, entonces la crearemos como una tarea en el archivo package.json:

  "scripts": {
    "start-server": "live-server",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

Ahora para iniciar el servidor:

$ npm run start-server

Entonces, para ejecutar cualquier script del archivo package.json simplemente es necesario utilizar npm run <nombre-script>.

Y eso es todo por ahora, a golpear el teclado!!!! >_<







domingo, 19 de marzo de 2017

Patrones de diseño (Builder Pattern) - ¿Qué hacer cuando se tiene demasiados parámetros en el constructor?

El patrón de construcción (Builder Pattern) es un patrón creacional que se utiliza cuando se tiene demasiados parámetros en un constructor. De acuerdo a Robert C. Martin en su libro Clean Code (lectura sumamente aconsejable), el número ideal de argumentos que se debe enviar a una función es cero, luego un parámetro, luego dos y luego tres. En caso de necesitar una mayor cantidad de parámetros se debe buscar una muy buena justificación para utilizarlos y aún así no utilizarlos :D

Entonces, ¿qué hacemos cuando necesitamos construir una Objeto con bastantes parámetros y se necesita mucha lógica de construcción? Builder Pattern al rescate!!!

Primero veremos un ejemplo de la construcción de una persona de la forma convencional:

function Persona(nombre, apellidos, edad,
                 sexo, altura, tonoDePiel, tipoDeCabello) {
  this.nombre = nombre;
  this.apellidos = apellidos;
  this.edad = edad;
  this.sexo = sexo;
  this.altura = altura;
  this.tonoDePiel = tonoDePiel;
  this.tipoDeCabello = tipoDeCabello;
}

var persona1 = new Persona('Adrian', 'Espinoza', 32,
                            'Masculino', 1.80,
                            '#f3f312', 'lacio');
var persona2 = new Persona('Magda', 'Apaza', 12,
                            'Femenino', 1.60,
                            '#13f3f2', 'lacio');

console.log('+++++++++++++++++++++++++++++++++++++++++');
console.log(persona1);
console.log('+++++++++++++++++++++++++++++++++++++++++');
console.log(persona2);
console.log('+++++++++++++++++++++++++++++++++++++++++');

¿Funciona? Claro que funciona, pero... ¿qué pasaría si quiero agregar un atributo? tendría que modificar tanto Persona como sus creaciones, y ¿si modifico el orden de los atributos? :O se vueve cada vez más complicado el mantenimiento. Ahora veamos el mismo ejemplo con el Builder Pattern:

// Persona builder
function PersonaBuilder() {
  // atributos del builder
  var nombre;
  var apellidos;
  var edad;
  var sexo;
  var altura;
  var tonoDePiel;
  var tipoDeCabello;

  // Objeto persona
  function Persona(personaBuilder) {
    this.nombre = personaBuilder.getNombre();
    this.apellidos = personaBuilder.getApellidos();
    this.edad = personaBuilder.getEdad();
    this.sexo = personaBuilder.getSexo();
    this.altura = personaBuilder.getAltura();
    this.tonoDePiel = personaBuilder.getTonoDePiel();
    this.tipoDeCabello = personaBuilder.getTipoDeCabello();
  }

  // getters, setters y builder
  return {
    setNombre: function (nombre) {
      this.nombre = nombre;
      return this;
    },
    getNombre: function() {
      return this.nombre;
    },
    setApellidos: function (apellidos) {
      this.apellidos = apellidos;
      return this;
    },
    getApellidos: function() {
      return this.apellidos;
    },
    setEdad: function (edad) {
      this.edad = edad;
      return this;
    },
    getEdad: function() {
      return this.edad;
    },
    setSexo: function (sexo) {
      this.sexo = sexo;
      return this;
    },
    getSexo: function() {
      return this.sexo;
    },
    setAltura: function (altura) {
      this.altura = altura;
      return this;
    },
    getAltura: function() {
      return this.altura;
    },
    setTonoDePiel: function (tonoDePiel) {
      this.tonoDePiel = tonoDePiel;
      return this;
    },
    getTonoDePiel: function() {
      return this.tonoDePiel;
    },
    setTipoDeCabello: function (tipoDeCabello) {
      this.tipoDeCabello = tipoDeCabello;
      return this;
    },
    getTipoDeCabello: function() {
      return this.tipoDeCabello;
    },
    build: function () {
      return new Persona(this);
    }
  };
}

// Ahora crear personas ss mucho más simple, además el ordén de los 
// atributos no importa acá
var personaBuilder = new PersonaBuilder();
var persona1 = personaBuilder.setNombre('Jhon')
                            .setApellidos('Perez Apaza')
                            .setEdad(25)
                            .setAltura(1.75)
                            .setSexo('Masculino')
                            .setTonoDePiel('#fa45fc')
                            .setTipoDeCabello('lacio')
                            .build();

var persona2 = personaBuilder.setNombre('Jackie')
                              .setApellidos('Sullivan')
                              .setTipoDeCabello('lacio')
                              .setSexo('Femenino')
                              .setEdad(45)
                              .setAltura(1.50)
                              .setTonoDePiel('#aaa53c')
                              .build();

console.log('+++++++++++++++++++++++++++++++++++++++++');
console.log(persona1);
console.log('+++++++++++++++++++++++++++++++++++++++++');
console.log(persona2);
console.log('+++++++++++++++++++++++++++++++++++++++++');


Ahora es mucho más sencillo realizar cambios y mantenerlos controlados, además, el ordén en que se agregan los atributos ya no importa.

Salida del script anterior

Y eso es todo por ahora. Cabe destacar que no es necesario utilizar cada patrón en todas las ocasiones que se presentan, ya que los patrones agregan complejidad que no siempre es necesario en un proyecto. Este ejemplo puede utilizarse en un proyecto donde sea necesario crear muchas personas y con muchos atributos, pero esto no sucede en todo slos proyectos que estamos utilizando. Para evitar crear un ibjeto con demasiados parámetros también se pueden ir agrupando los parámetros en objetos pequeños para luego construir el objeto completo.

Y eso es todo, recuerden, KISS (Keep It Simple S...) y Keep Coding XD

Nuevas características de ecma6 - Generadores e Iteradores

Hello!!! hoy hablaremos un poco de una nueva característica muy interesante en javascript (ecma6), los generadores.

En términos simples, un generador es una función que genera una serie de valores, con la particularidad de que no todos se generan al mismo tiempo, sino, de acuerdo a lo que se solicite. Es necesario pedir explicitamente valores a un generador, y el generador nos respondera con un nuevo valor o nos notificará que no existen más valores que retornar.

Además, un iterador es un objeto que sabe como acceder a los elementos de una colección uno a uno y que mantiene un registro de su posición en la secuencia :O. Los iteradores proveen el método next() para obtener el siguiente valor de la secuencia. next() devuelve un objeto con las propiedades done y value;

Una característica muy particular de los generadores es que cuando devuelven un valor, no téminan su labor como una función normal, sino que simplemente entran en un estado de suspensión, luego, cuando solicitamos un nuevo valor, el mismo generador reanuda su labor donde se detuvo anteriormente para luego volver a detenerse :O

Dado que siempre es mucho más sencillo entender algo con ejemplos, escribiremos una función que vaya generando números múltiplos de 3 primero y de siete a continuación.

// es necesario utilizar polyfills para que los generadores funcionen
import 'babel-polyfill';

console.log('Iniciando pruebas con generadores');

// agregamos un * para crear un generador
function* generador() {
  for(let i = 1; i <= 3; i++) {
    // utilizamos yield para devolver un valor
    // y pausar la ejecución del generador
    yield i * 3;
  }
  for(let i = 1; i <= 3; i++) {
    yield i * 7;
  }
}

console.log('\nPrimer bucle while\n');

// obtenemos el iterador del generador
const iterator = generador();
let element;
do{
  // obtenemos un valor del iterador
element = iterator.next();
console.log(element); // cuando no hay mas elementos nos detenemos (element.done === true) } while(!element.done); console.log('\nSegundo bucle while\n'); const iterator2 = generador(); let element2; while(!(element2 = iterator2.next()).done ) { console.log(element2); }

Para definir un generador simplemente le agregamos un * a la palabra reservada function (function*). Con esto ya tenemos definido un generador y podemos utilizar la palabra reservada yield para devolver valores. Al llamar a la función no estamos ejecutandola, simplemente estamos creando un iterador, y ya con el iterador podemos utilizar next() para obtener un valor.
Como podemos ver en la imagen, obtenemos los valores del generador correctamente y cuando ya no existen más valores, la propiedad done es true.


Resultado de la ejecución del script

Veamos otro ejemplo interesante, ahora realizaremos la iteración del generador con for-of y revisaremos el operador de desestructuración:

import 'babel-polyfill';

// definimos el generador
function* generador() {
  yield 'Hola ';
  yield 'Mundo ';
  yield 'Con ';
  yield 'Generadores ';
}

console.log('============= Iterando con for of =============');
for (let palabra of generador()) {
  console.log(palabra);
}

console.log('\n============= Con el operador de propagación =============');
// ... permite a al generador expandirse y se almacena en arr
const arr = [...generador()];
console.log(arr)

console.log('\n============= Desestructurando =============');
// propaga el generador en los valores de p1 a p4 (el generador tiene 4 yields)
// como no existe un quinto valor en el generador p5 imprime undefined
const [p1, p2, p3, p4, p5] = [...generador()]
console.log(p1);
console.log(p2);
console.log(p3);
console.log(p4);
// imprimira indefinido porque el generador solamente devuelve cuatro valores
console.log(p5);

El resultado del proceso es:
Resultado del proceso
El operador spread (...) permite al generador ejecutar todos los valores que tenga; además con la desestructuración podemos asignar los valores a variables como se ve en el segundo ejemplo.

Y eso es todo por ahora, espero les haya parecido tan interesante como a mi :p keep coding!!!

martes, 7 de marzo de 2017

Programación asíncrona con Javascript - Parte 2, Promesas (lo básico con ejemplos)

Hola de nuevoooo :D es hora de hablar un poco de las promesas, una promesa es asíncrona por naturaleza, representa un resultado que puede estar disponible ahora, en el futuro o nunca. Una promesa puede resolverse (devolver un resultado) o rechazarse (lanzar un error).

Crear una promesa en javascript es bastante simple, solamente es necesario pasar una función ejecutora que recibe dos parámetros, el primer parámetro es una función que debe llamarse cuando se resuelva la promesa (cuando se quiere devolver un resultado de la operación asíncrona) y el segundo parámetro es la función que se llamará en caso de que la promesa no se cumpla. Tiene la forma new Promise(function(resolver, rechazar){}). Como ejemplo:

// ahora creamos una promesa
var promesa = new Promise(function(resolve, reject) {
  // realizar alguna tarea
  var tareaExitosa = realizarMuchasOperaciones();
  if (tareaExitosa) {
    // resolvemos la promesa en caso de éxito
    resolve('Éxito');
  } else {
    // rechazamos la promesa en caso de error
    reject(new Error('Error al realizar la operación.'));
  }
});

Además, es posible crear promesas ya resueltas o rechazadas:

// para crear una promesa que sea exitosa
Promise.resolve("Promesa exitosa");
// para crear una promesa que sea rechazada
Promise.reject(new Error('Promesa rechazada'));

Cuando se quiere realizar alguna acción o transformación con los resultados de la promesa, se le añade el método then(). then() también nos ayuda a encadenar las promesas una tras otra.

Toda cadena de promesas debe tener el método catch() al final para poder agarrar cualquier error que pudiera presentarse en el proceso.

Por ejemplo, realizaremos una promesa que calculará asíncronamente un número entre 0 y 10, si el número es mayor a 5 la promesa se resuelve correctamente, si el número es menor a 5 la promesa se rechaza.

var promise = new Promise(function(resolve, reject) {
  setTimeout(function(){
    // calculamos un npumero aleatorio entre 0 y 10
    var num = parseInt( Math.random() * 10 );
    if ( num > 5) {
      // la promesa se resuelve si num es mayoa  5
      resolve('Promesa resuelta correctamente, el número es ' + num);
    } else {
      // la promesa se rechaza si numero es menor a cinco
      reject(new Error('Ha ocurrido un error, el número calculado es ' + num));
    }

  }, 2000);
});

// manejamos la promesa
promise.then(function(val){
  // la promesa se ha resuelto
  console.log(val);
}).catch(function(err){
  // la promesa ha sido rechazada
  console.log(err);
});

then() puede encadenarse, y debe devolver un valor (resolve) o debe lanzar un error (el error se pasará a catch).

var promise = new Promise(function(resolve, reject) {
  setTimeout(function(){
    resolve(10);
  }, 2000);
});

promise.then(function(val){
  // retorna un valor, por lo tanto la promesa se resuelve implicitamente
  // y pasará al siguiente then
    return val * 10;
  })
  .then(function(val2) {
    // resolvemos la promesa explicitamente
    // el resultado se pasa al siguiente then
    return Promise.resolve(val2 / 2);
  })
  .then(function(val3) {
    // imprimimos el resultado
    return console.log(val3)
  })
  .catch(function(err){
    // como no hubo error, nunca se imprime
    console.log(err);
  });

En caso de surgir un error (previsto o imprevisto), los siguientes then() no se ejecutan y se pasa directamente al catch().

Por ejemplo, lanzaremos un error en una promesa para ver el ordén en que se ejecutan las operaciones.

var promise = new Promise(function(resolve, reject) {
  setTimeout(function(){
    resolve(10);
  }, 2000);
});

promise.then(function(val){
    console.log('Primer then');
    return val * 10;
  })
  .then(function(val2) {
    console.log('Segundo then');
    if (val2 === 100) {
      throw new Error("Unaceptable, no permitimos el número cien en mi reino");
    }
    return Promise.resolve(val2 / 2);
  })
  .then(function(val3) {
    // imprimimos el resultado
    console.log('Tercer then');
    return console.log(val3)
  })
  .catch(function(err){
    // como no hubo error, nunca se imprime
    console.log('Catch');
    console.log(err);
  });

El resultado que nos muestra es:

Resultado de la ejecución de las promesas
Como podemos observar, se ejecutan el primer y el segundo then(), sin embargo, el tercer then() no se ejecuta pues lanzamos un error, entonces el proceso pasa directamente a catch().

Y eso es todo por ahora, en el siguiente post veremos conceptos más avanzados sobre promesas.

Saludos y keep coding!!!  ( ° _ ° )

Programación asíncrona con Javascript - Parte 1, lo básico (event queue, call stack, event loop, timers y DOM events)

Hola de nuevo, ya que últimamente he estado escribiendo bastante sobre javascript es necesario, para comprender mejor lo que se hace, conocer que es la programación asíncrona (pista: la programación asincrona permite realizar operaciones en segundo plano :D).

Pero primero, ¿porqué es necesaria la programación asíncrona?, el problema es que javascript es single threaded (se ejecuta sobre un solo hilo), lo que imposibilita realizar más de una tarea a la vez. Dado que las tareas se ejecutan una tras otra y que se tiene un entorno single threaded, el hecho de que una tome demasiado tiempo para realizarse evita que las demas tareas se ejecuten y es por esto que los procesos que comunmente tardan (I/O) se ejecutan asíncronamente, para evitar que se tenga que bloquear la realización de otras tareas.

Un ejemplo de un proceso que toma demasiado tiempo en ejecutarse y bloqueara la ejecución del programa hasta que finalice:

function tareaCorta(param) {
  console.log('Esta ejecutandosé la tarea corta ' + param);
}

function tareaLarga(param) {
  console.log('Esta ejecutandosé la tarea larga ' + param);
  for(var i = 0; i <= 5000000000; i++) {
    // bucle que tarda demasiado en ejecutarse
  }
  console.log('Terminó la tarea larga ' + param);
}

tareaCorta(1);
tareaCorta(2);
tareaLarga(3);
tareaCorta(4);
tareaCorta(5);

Y como se puede observar, el programa se detiene para ejecutar la tarea larga (que es simplemente un bucle muuuuuy largo):
Ejemplo de la ejecución del código
No siempre es sencillo comprender el concepto de asincronía, así que diagramaremos el comportamiento en el tiempo del siguiente fragmento de pseudo-código.

TAREA1();
RESULTADO_TAREA_2 = TAREA2();
TAREA3(RESULTADO_TAREA_2);
TAREA4();

Mensajes asíncronos y síncronos
Como se puede observar en la línea de tiempo del gráfico (cada flecha indica la existencia de un hilo), en el estilo síncrono cada tarea debe esperar a que la anterior concluya para realizar otra operación, sin embargo, introduciendo la asincronía si se tiene una operación que consume demasiado tiempo (comunmente I/O), la tarea se ejecuta de manera asíncrona (en otro entorno de ejecución), y luego se puede completar el proceso (usualmente al terminar la tarea asincrona se agrega el callback respectivo al event queue con los resultados de la operación asíncrona). En la imagen se puede observar que dado que la tarea4 no depende de la ejecución de la tarea2, esta se lanza inmediatamente aunque la tarea anterior no haya finalizado.

Para conceptualizar mejor la idea, es necesario estudiar el siguiente gráfico del mismo pseudocógido anterior:
Ejecución asíncrona vs síncrona
Esta vez el tiempo de espera de la petición por la red se ha reducido, y si bien en el primer caso (síncrono) la ejecución no varia demasiado, se puede observar que en el caso de la ejecución asíncrona, aunque la petición por la red ya ha terminado, la tarea3 no puede ejecutarse hasta que la tarea4 haya concluido porque nuestro entorno de ejecución es single threaded. La idea se resume en: todo corre en un hilo distinto excepto nuestro código.

Ahora, algunos conceptos que son de importancia:
  • call stack (pila de llamadas): mantiene un listado de las funciones que están ejecutandose. Si la funcion1 llama a la funcion2, entonces en el callstack está ejecutandose la funcion2 y está en espera en el mismo call stack la funcion1.
  • event queue (cola de eventos): es una cola que mantiene mensajes que referencian a las funciones que estan en espera de ser puestas en el call stack para ser ejecutadas. Las tareas son agregadas a la cola de eventos por web apis que corren en paralelo con el entorno de javascript.
    • web api: agregan tareas al event queue, por ejemplo:
      • timers: programan tareas para ser agregadas al event queue en un determinado tiempo.
      • DOM event handlers: agrega interacciones del usuario, como eventos del mouse o del teclado al event queue.
      • network requests: las peticiones por la red son procesadas asincronamente y devuelven resultados agregando tareas al event queue.
  • event loop (ciclo de eventos): cuando el call stack esta vacio, toma la primera tarea del event queue y la procesa. Las tareas que quedan en la cola (queue) esperan hasta que el call stack esta limpio nuevamente. Este ciclo se llama event loop.

Ejemplo del event queue y el call stack:

function func1(){
  console.log('tarea1');
}

function func2(){
  console.log('tarea1');
}

func1();
func2();

Ejemplo del call stack y del event queue
Ese fue un ejemplo bastante simple, como se puede observar las tareas van ejecutandosé una a una en el call stack hasta que este queda vacio nuevamente. Pero, ¿qué pasaría si las tareas estuvieran relacionadas?

function func1(){
  func2();
}

function func2(){
  console.log('Tarea completadas');
}

func1();

Ejemplo del event queue y call stack
En este último caso podemos ver como se comportan lo seventos cuando estan relacionados, se van agregando al call stack todas las tareas necesarias para que termine la primera tarea que se insertó. En el transcurso del tiempo en que tardan en ejecutarse estas tareas relacionadas, otros eventos pudieron agregar tareas al call stack.

Es posible que las apis web agreguen los resultados de sus procesos como tareas a la cola de eventos (event queue). Estas tareas son definidas por funciones callbacks que se pasan a las apis web.

- callback functions: Son funciones que se pasan como argumentos para ser ejecutadas en un punto futuro del tiempo.

- anonymous callbacks: Son funciones creadas sin nombre. Son útiles cuando el callback solamente necesita ser ejecutado una vez.

Como ejemplo, veamos una función callback con nombre y una función callback anónima:

var numeros = [1, 2, 3, 4, 5];
console.log(numeros);

// callback anónimo, multiplica por dos
var numerosDuplicados = numeros.map(function(valor) {
  return valor * 2;
});
console.log(numerosDuplicados);

function multiplicaPorTres (valor) {
  return valor * 3;
}
// callback con una función que tiene un nombre
var numerosTriplicados= numeros.map(multiplicaPorTres);
console.log(numerosTriplicados);

Timers
Son funciones nativas de javascript que permiten retrasar la ejecución de instrucciones de la manera que necesitemos. El tiempo que necesitamos que se retrasen se calcula asíncronamente.
  • setInterval() se utiliza para programar una tarea recurrente en el tiempo
  • clearInterval() se utiliza para detener una tarea que se creó con setInterval()
  • setTimeout() se  utiliza para programar una tarea en un determinado tiempo
Ahora como ejemplo ejecutaremos la misma tarea del primer ejemplo, pero ahora la primera tarea se retrase 1000 milisegundos (1 segundo).

function tareaCorta(param) {
  console.log('Esta ejecutandosé la tarea corta ' + param);
}

function tareaLarga(param) {
  console.log('Esta ejecutandosé la tarea larga ' + param);
  for(var i = 0; i <= 5000000000; i++) {
    // bucle que tarda demasiado en ejecutarse
  }
  console.log('Terminó la tarea larga ' + param);
}

setTimeout(function(){
  tareaCorta(1)
}, 5000);
tareaCorta(2)
tareaLarga(3);
tareaCorta(4);
tareaCorta(5);

El resultado de la ejecución es el siguiente:

Ejecución con timeout
Como se puede observar, si bien se programó la tarea para que se ejecute en 5 segundos.
El timer funciona correctamente, la tarea se ejecuta cinco segundos despues. Ahora, veamos otro ejemplo un poco mas extraño:

function tareaCorta(param) {
  console.log('Esta ejecutandosé la tarea corta ' + param);
}

function tareaLarga(param) {
  console.log('Esta ejecutandosé la tarea larga ' + param);
  for(var i = 0; i <= 5000000000; i++) {
    // bucle que tarda demasiado en ejecutarse
  }
  console.log('Terminó la tarea larga ' + param);
}

setTimeout(function(){
  tareaCorta(1)
}, 0);
tareaCorta(2)
tareaLarga(3);
tareaCorta(4);
tareaCorta(5);

Como podemos observar, ahora establecimos el timeout a 0, o sea que la tarea se programa para ejecutarse de inmediato. Veamos el resultado:
Resultado de la ejecución
Lo interesante del ejemplo es que la tareaCorta(1) aún se ejecuta al final, pero ¿porqué, si su timeout (tiempo de espera) es cero?, el problema es el orden en el que se agregan las tareas al callstack, veamos (Revisar el diagrama de Ejecución asíncrona vs síncrona que se agrego en uno de los puntos anteriores):

  • Se "programa" la ejecución del timeout en 0 segundos (la tarea ya esta lista para agregarse, pero estamos esperando al siguiente ciclo de eventos)
  • Se agrega al call stack la tareaCorta(2) para su ejecución
  • Se agrega al call stack la tareaLarga(3) para su ejecución
  • Se agrega al call stack la tareaCorta(4) para su ejecución
  • Se agrega al call stack la tareaCorta(5) para su ejecución (ya no existen más elementos para ejecutarse en el call stack así que ahora se agrega la tarea que se programó antes)
  • Se agrega al call stack la tareaCorta(1) para su ejecución

Se programa el timeout, se agregan al call stack las cuatro tareas y luego se agrega la tarea que esta esperando a ser ejecutada por el timeout con 9 milisegundos de espera.

DOM event listeners

Los "oyentes" de eventos del DOM ocurren paralelamente al cógido javascript ejecutado (asíncronamente). Los event listeners detectan un evento y lo agregan al event queue para que el evento sea ejecutado en el futuro. En caso de tener muchos eventos que suceden uno tras otro, todos se agregan en el orden en que ocurren a la cola de eventos (event queue). Algunos eventos que pueden suceder son: keypressed, keyup, mouseenter, mouseleave, etc.

Ahora un ejemplo de el manejo de eventos con javascript super sencillo (lo importante es recordar que los listeners siempre estan escuchando y agregarán el evento al event queue en cualquier momento). Crear en una carpeta el archivo example.html y example.js. El código del archivo example.html es:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Eventos</title>
</head>
<body>
    <div id="div-rojo" style="background-color: red;height:200px;"></div>
    <br />
    <div id="elemento"></div>
    <script src="example.js"></script>
</body>
</html>

Simplemente creamos dos divs, y agregamos el javscript con el nombre "example.js". Ahor el código javascript:

document.getElementById('div-rojo')
  .addEventListener('mouseenter', function(){
    document.getElementById('elemento')
            .innerHTML = 'El mouse ha entrado al div rojo';
});

document.getElementById('div-rojo')
  .addEventListener('mouseleave', function(){
    document.getElementById('elemento')
            .innerHTML = 'El mouse ha salido del div rojo';
});

Con esto, obtenemos el primer div (div-rojo), y le agregamos un listener para el evento mouseenter, cuando el evento suceda, obtenemos el otro div y escribimos el mensaje El mouse ha entrado al div rojo. También, obtenemos el primer div (div-rojo) y le agregamos el evento contrario (mouseleave), pero para que se note el cambio modificamos el mensaje del otro div diciendo que El mouse ha salido del div rojo.

Ejemplo de ejecución (no tomen en cuenta las manchas, es para mantener bajo el tamaño del gif XD)
Y eso es todo por ahora, keep coding!!! :)