Comprender las Promesas en JavaScript

Introducción

Las Promesas de Javascript pueden ser difíciles de entender. Por lo tanto, me gustaría escribir la forma en que entiendo las promesas.

Entendiendo las Promesas

Una Promesa en resumen:

“Imagina que eres un niño. Tu mamá promete que te comprará un nuevo teléfono la próxima semana.”

No sabes si obtendrás ese teléfono hasta la próxima semana. Tu mamá puede realmente comprar un teléfono nuevo para ti, o no lo hace.

Eso es una promesa. Una promesa tiene tres estados. Ellos son:

  1. Pendiente: No sabes si obtendrás ese teléfono
  2. Cumplida: Mamá está contenta, te compra un teléfono nuevo
  3. Rechazada: Mamá está infeliz, no te compra un teléfono

Creando una Promesa

Vamos a convertir esto en JavaScript.

// ES5: Parte 1

var isMomHappy = false;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // cumplido
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // rechazar
        }

    }
);

El código es bastante expresivo en sí mismo.

A continuación se muestra cómo se ve normalmente la sintaxis de una promesa:

// la sintaxis de la promesa se ve así
new Promise(function (resolve, reject) { ... } );

Consumir Promesas

Ahora que tenemos la promesa, consumámosla:

// ES5: Parte 2

var willIGetNewPhone = ... // continuar desde la parte 1

// llamar a nuestra promesa
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // yay, tienes un nuevo teléfono
            console.log(fulfilled);
             // salida: { marca: 'Samsung', color: 'negro' }
        })
        .catch(function (error) {
            // oops, mamá no lo compró
            console.log(error.message);
             // salida: 'mamá no está feliz'
        });
};

askMom();

¡Ejecutemos el ejemplo y veamos el resultado!

Demo: https://jsbin.com/nifocu/1/edit?js,console

Encadenamiento de Promesas

Las promesas son encadenables.

Digamos que tú, el niño, prometes a tu amigo que se lo mostrarás el nuevo teléfono cuando tu mamá te lo compre.

Esa es otra promesa. Escribámosla!

// ES5

// 2da promesa
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

Notas: Podemos acortar el código anterior escribiéndolo como se muestra a continuación:

// acortarlo

// 2da promesa
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

Vamos a encadenar las promesas. Tú, el niño, solo puedes comenzar la promesa showOff después de la promesa willIGetNewPhone.

// llamar a nuestra promesa
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // encadenarla aquí
    .then(function (fulfilled) {
            console.log(fulfilled);
         // salida: 'Oye amigo, tengo un nuevo teléfono Samsung negro.'
        })
        .catch(function (error) {
            // ups, mamá no lo compra
            console.log(error.message);
         // salida: 'mamá no está feliz'
        });
};

Así es como puedes encadenar la promesa.

Las promesas son asincrónicas

Las promesas son asincrónicas. Vamos a registrar un mensaje antes y después de llamar a la promesa.

// llamamos a nuestra promesa
var askMom = function () {
    console.log('before asking Mom'); // registro antes
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // registro después
}

¿Cuál es la secuencia de salida esperada? Podrías esperar:

1. before asking Mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom

Sin embargo, la secuencia de salida real es:

1. before asking Mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.

No dejarías de jugar mientras esperas la promesa de tu mamá (el nuevo teléfono). Eso es algo que llamamos asincrónico: el código se ejecutará sin bloquearse ni esperar el resultado. Cualquier cosa que necesite esperar a que se resuelva una promesa para continuar se pone en .then.

Aquí está el ejemplo completo en ES5:

// ES5: Ejemplo completo

var isMomHappy = true;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // cumplida
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // rechazar
        }

    }
);

// 2da promesa
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

// llamar nuestra promesa
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // encadenarlo aquí
    .then(function (fulfilled) {
            console.log(fulfilled);
            // salida: 'Hola amigo, tengo un nuevo teléfono Samsung negro.'
        })
        .catch(function (error) {
            // oops, mamá no lo compra
            console.log(error.message);
            // salida: 'mamá no está feliz'
        });
};

askMom();

Promesas en ES5, ES6/2015, ES7/Next

ES5 – Mayoría de navegadores

El código de demostración es funcional en entornos ES5 (todos los principales navegadores + NodeJs) si incluyes la biblioteca de promesas Bluebird. Esto se debe a que ES5 no soporta promesas de forma nativa. Otra famosa biblioteca de promesas es Q de Kris Kowal.

ES6 / ES2015 – Navegadores modernos, NodeJs v6

El código de demostración funciona directamente porque ES6 soporta promesas de manera nativa. Además, con las funciones de ES6, podemos simplificar aún más el código con una función flecha y usar const y let.

Aquí está el ejemplo completo en código ES6:

//_ ES6: Ejemplo completo_

const isMomHappy = true;

// Promesa
const willIGetNewPhone = new Promise(
    (resolve, reject) => { // función flecha
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// 2da promesa
const showOff = function (phone) {
    const message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';
    return Promise.resolve(message);
};

// llamar a nuestra promesa
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // función flecha
        .catch(error => console.log(error.message)); // función flecha
};

askMom();

Ten en cuenta que todos los var han sido reemplazados con const. Todas las function(resolve, reject) se han simplificado a (resolve, reject) =>. Estos cambios traen varios beneficios.

ES7 – Async/Await

ES7 introdujo la sintaxis async y await. Hace que la sintaxis asíncrona sea más fácil de entender, sin los .then y .catch.

Reescribamos nuestro ejemplo con la sintaxis ES7:

// ES7: Ejemplo completo
const isMomHappy = true;

// Promesa
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// 2da promesa
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

// llamar a nuestra promesa en estilo ES7 async await
async function askMom() {
    try {
        console.log('before asking Mom');

        let phone = await willIGetNewPhone;
        let message = await showOff(phone);

        console.log(message);
        console.log('after asking mom');
    }
    catch (error) {
        console.log(error.message);
    }
}

// async await aquí también
(async () => {
    await askMom();
})();

Promesas y Cuándo Utilizarlas

¿Por qué necesitamos promesas? ¿Cómo era el mundo antes de las promesas? Antes de responder estas preguntas, regresemos a los fundamentos.

Función Normal VS Función Asíncrona

Veamos estos dos ejemplos. Ambos ejemplos realizan la suma de dos números: uno los suma utilizando funciones normales, y el otro los suma de manera remota.

Función Normal para Sumar Dos Números

// sumar dos números normalmente

function add (num1, num2) {
    return num1 + num2;
}

const result = add(1, 2); // obtienes result = 3 inmediatamente
Función Asíncrona para Sumar Dos Números
// sumar dos números de manera remota

// obtener el resultado llamando a una API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// obtienes result = "undefined"

Si sumas los números con la función normal, obtienes el resultado inmediatamente. Sin embargo, cuando haces una llamada remota para obtener el resultado, necesitas esperar, y no puedes obtener el resultado de inmediato.

No sabes si obtendrás el resultado porque el servidor podría estar inactivo, responder lentamente, etc. No quieres que todo tu proceso se bloquee mientras esperas el resultado.

Llamar a APIs, descargar archivos y leer archivos son algunas de las operaciones asíncronas habituales que realizarás.

No necesitas usar promesas para una llamada asíncrona. Antes de las promesas, usábamos callbacks. Los callbacks son una función que llamas cuando obtienes el resultado de retorno. Modifiquemos el ejemplo anterior para aceptar un callback.

// sumar dos números de manera remota
// obtener el resultado llamando a una API

function addAsync (num1, num2, callback) {
    // usar la famosa API de callback getJSON de jQuery
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback
    const result = success; // obtienes result = 3 aquí
});

Acción Asíncrona Subsiguiente

En lugar de sumar los números uno por uno, queremos sumar tres veces. En una función normal, haríamos esto:

// sumar dos números normalmente

let resultA, resultB, resultC;

 function add (num1, num2) {
    return num1 + num2;
}

resultA = add(1, 2); // obtienes resultA = 3 inmediatamente
resultB = add(resultA, 3); // obtienes resultB = 6 inmediatamente
resultC = add(resultB, 4); // obtienes resultC = 10 inmediatamente

console.log('total' + resultC);
console.log(resultA, resultB, resultC);

Así es como se ve esto con callbacks:

// sumar dos números de manera remota
// obtener el resultado llamando a una API

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // usar la famosa API de callback getJSON de jQuery
	// https://api.jquery.com/jQuery.getJSON/
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback 1
    resultA = success; // obtienes result = 3 aquí

    addAsync(resultA, 3, success => {
        // callback 2
        resultB = success; // obtienes result = 6 aquí

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // obtienes result = 10 aquí

            console.log('total' + resultC);
            console.log(resultA, resultB, resultC);
        });
    });
});

Demo: https://jsbin.com/barimo/edit?html,js,console

Esta sintaxis es menos amigable para el usuario debido a los callbacks anidados profundamente.

Evitar Callbacks Anidados Profundamente

Las promesas pueden ayudarte a evitar callbacks anidados profundamente. Veamos la versión con promesas del mismo ejemplo:

// sumar dos números de forma remota usando observable

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // usar la API fetch de ES6, que devuelve una promesa
	// ¿Qué es .json()? https://developer.mozilla.org/es/docs/Web/API/Body/json
    return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json()); 
}

addAsync(1, 2)
    .then(success => {
        resultA = success;
        return resultA;
    })
    .then(success => addAsync(success, 3))
    .then(success => {
        resultB = success;
        return resultB;
    })
    .then(success => addAsync(success, 4))
    .then(success => {
        resultC = success;
        return resultC;
    })
    .then(success => {
        console.log('total: ' + success)
        console.log(resultA, resultB, resultC)
    });

Con las promesas, aplanamos la devolución de llamada con .then. De alguna manera, parece más limpio porque no hay anidamiento de devoluciones de llamada. Con la sintaxis async de ES7, podrías mejorar aún más este ejemplo.

Observables

Antes de decidirte por las promesas, existe algo que ha surgido para ayudarte a manejar datos asíncronos llamado Observables.

Veamos el mismo ejemplo escrito con Observables. En este ejemplo, usaremos RxJS para los observables.

let Observable = Rx.Observable;
let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // usar la API fetch de ES6, que devuelve una promesa
    const promise = fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json());

    return Observable.fromPromise(promise);
}

addAsync(1,2)
  .do(x => resultA = x)
  .flatMap(x => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total: ' + x)
    console.log(resultA, resultB, resultC)
  });

Los Observables pueden hacer cosas más interesantes. Por ejemplo, delay añadir función por 3 segundos con solo una línea de código o reintentar para que puedas reintentar una llamada un cierto número de veces.

...

addAsync(1,2)
  .delay(3000) // retrasar 3 segundos
  .do(x => resultA = x)
  ...

Puedes leer uno de mis posts sobre RxJs aquí.

Conclusión

Es importante familiarizarse con las callbacks y las promesas. Entiéndelas y utilízalas. No te preocupes por los Observables por ahora. Las tres pueden influir en tu desarrollo dependiendo de la situación.

Source:
https://www.digitalocean.com/community/tutorials/understanding-javascript-promises