Entendendo Promessas em JavaScript

Introdução

Promessas em Javascript podem ser desafiadoras de entender. Portanto, gostaria de escrever a maneira como entendo as promessas.

Entendendo Promessas

Uma Promessa em resumo:

“Imagine que você é uma criança. Sua mãe promete que vai te dar um novo telefone na próxima semana.”

Você não sabe se vai receber aquele telefone até a próxima semana. Sua mãe pode realmente comprar um telefone novo para você, ou ela não compra.

Isso é uma promessa. Uma promessa tem três estados. Eles são:

  1. Pendente: Você não sabe se vai receber aquele telefone
  2. Cumprida: A mãe está feliz, ela compra um telefone novo para você
  3. Rejeitada: A mãe está infeliz, ela não compra um telefone para você

Criando uma Promessa

Vamos converter isso para JavaScript.

// ES5: Part 1

var isMomHappy = false;

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

    }
);

O código é muito expresso por si próprio.

Abaixo é como uma sintaxe de promessa parece normalmente:

// sintaxe de promessa normalmente assim
new Promise(function (resolve, reject) { ... } );

Consumir Promessas

Agora que temos a promessa, vamos consumirla:

// ES5: Part 2

var willIGetNewPhone = ... // continuar da parte 1

// chamar nossa promessa
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // yay, você ganhou um novo celular
            console.log(fulfilled);
             // saída: { marca: 'Samsung', cor: 'preto' }
        })
        .catch(function (error) {
            // ops, a mãe não comprou
            console.log(error.message);
             // saída: 'mãe não está feliz'
        });
};

askMom();

Vamos executar o exemplo e ver o resultado!

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

Encadeamento de Promessas

Promessas são encadeáveis.

Digamos que você, a criança, prometa a um amigo que vai mostrar a eles o novo telefone quando sua mãe comprar um para você.

Isso é outra promessa. Vamos escrevê-la!

// ES5

// 2ª promessa
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 encurtar o código acima escrevendo da seguinte forma:

// encurtar

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

    return Promise.resolve(message);
};

Vamos encadear as promessas. Você, a criança, só pode iniciar a promessa showOff após a promessa willIGetNewPhone.

// chamar nossa promessa
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // encadeá-la aqui
    .then(function (fulfilled) {
            console.log(fulfilled);
         // saída: 'Ei amigo, eu tenho um novo telefone Samsung preto.'
        })
        .catch(function (error) {
            // ops, a mãe não compra
            console.log(error.message);
         // saída: 'a mãe não está feliz'
        });
};

Assim é como você pode encadear a promessa.

Promessas são Assíncronas

Promessas são assíncronas. Vamos registrar uma mensagem antes e depois de chamarmos a promessa.

// chamar nossa promessa
var askMom = function () {
    console.log('before asking Mom'); // registrar antes
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // registrar depois
}

Qual é a sequência de saída esperada? Você pode esperar:

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

No entanto, a sequência de saída real é:

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

Você não pararia de brincar enquanto esperava pela promessa da sua mãe (o novo telefone). Isso é algo que chamamos de assíncrono: o código será executado sem bloquear ou esperar pelo resultado. Qualquer coisa que precise esperar pela promessa para prosseguir é colocada em .then.

Aqui está o exemplo completo em ES5:

// ES5: Exemplo completo

var isMomHappy = true;

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

    }
);

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

    return Promise.resolve(message);
};

// chamar nossa promise
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // encadeá-la aqui
    .then(function (fulfilled) {
            console.log(fulfilled);
            // saída: 'Ei amigo, tenho um novo telefone Samsung preto.'
        })
        .catch(function (error) {
            // ops, mãe não comprou
            console.log(error.message);
            // saída: 'mãe não está feliz'
        });
};

askMom();

Promessas em ES5, ES6/2015, ES7/Next

ES5 – Maioria dos navegadores

O código de demonstração é viável em ambientes ES5 (todos os principais navegadores + NodeJs) se você incluir a biblioteca de promessas Bluebird. Isso porque o ES5 não suporta promessas prontas para uso. Outra famosa biblioteca de promessas é o Q por Kris Kowal.

ES6 / ES2015 – Navegadores modernos, NodeJs v6

O código de demonstração funciona imediatamente porque o ES6 suporta promises de forma nativa. Além disso, com funções ES6, podemos simplificar ainda mais o código com uma função de seta e usar const e let.

Aqui está o exemplo completo em código ES6:

//_ ES6: Exemplo completo_

const isMomHappy = true;

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

    }
);

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

// chamar nossa promise
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // função de seta
        .catch(error => console.log(error.message)); // função de seta
};

askMom();

Note que todos os var foram substituídos por const. Todas as function(resolve, reject) foram simplificadas para (resolve, reject) =>. Há vários benefícios que vêm com essas mudanças.

ES7 – Async/Await

O ES7 introduziu a sintaxe async e await. Isso torna a sintaxe assíncrona mais fácil de entender, sem os .then e .catch.

Reescreva nosso exemplo com a sintaxe ES7:

// ES7: Exemplo completo
const isMomHappy = true;

// Promise
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);
        }

    }
);

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

            resolve(message);
        }
    );
};

// chamar nossa promise no 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 aqui também
(async () => {
    await askMom();
})();

Promessas e Quando Utilizá-las

Por que precisamos de promessas? Como era o mundo antes das promessas? Antes de responder a essas perguntas, vamos voltar aos fundamentos.

Função Normal VS Função Assíncrona

Vamos analisar esses dois exemplos. Ambos os exemplos realizam a adição de dois números: um adiciona usando funções normais e o outro adiciona remotamente.

Função Normal para Adicionar Dois Números

// adicionar dois números normalmente

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

const result = add(1, 2); // você obtém result = 3 imediatamente
Função Assíncrona para Adicionar Dois Números
// adicionar dois números remotamente

// obter o resultado chamando uma API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// você obtém result = "undefined"

Se você adicionar os números com a função normal, obtém o resultado imediatamente. No entanto, quando você faz uma chamada remota para obter o resultado, precisa esperar, e não pode obter o resultado imediatamente.

Você não sabe se vai obter o resultado porque o servidor pode estar inativo, lento na resposta, etc. Você não quer que todo o seu processo seja bloqueado enquanto espera pelo resultado.

Chamar APIs, baixar arquivos e ler arquivos são algumas das operações assíncronas comuns que você realizará.

Você não precisa usar promessas para uma chamada assíncrona. Antes das promessas, usávamos callbacks. Callbacks são uma função que você chama quando obtém o resultado de retorno. Vamos modificar o exemplo anterior para aceitar um callback.

// adicionar dois números remotamente
// obter o resultado chamando uma API

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

addAsync(1, 2, success => {
    // callback
    const result = success; // você obtém result = 3 aqui
});

Ação Assíncrona Subsequente

Em vez de adicionar os números um de cada vez, queremos adicionar três vezes. Em uma função normal, faríamos isso:-

// adicionar dois números normalmente

let resultA, resultB, resultC;

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

resultA = add(1, 2); // você obtém resultA = 3 imediatamente
resultB = add(resultA, 3); // você obtém resultB = 6 imediatamente
resultC = add(resultB, 4); // você obtém resultC = 10 imediatamente

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

Assim é como fica com callbacks:

// somar dois números remotamente
// obter o resultado chamando uma API

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // usar a famosa API de callback getJSON do 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; // você obtém result = 3 aqui

    addAsync(resultA, 3, success => {
        // callback 2
        resultB = success; // você obtém result = 6 aqui

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // você obtém result = 10 aqui

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

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

Essa sintaxe é menos amigável para o usuário devido aos callbacks aninhados profundamente.

Evitando Callbacks Aninhados Profundamente

Promessas podem ajudar a evitar callbacks aninhados profundamente. Vamos ver a versão com promessas do mesmo exemplo:

// adicionar dois números remotamente usando observable

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // usar a API fetch do ES6, que retorna uma promise
	// O que é .json()? https://developer.mozilla.org/pt-BR/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)
    });

Com promises, nivelamos o callback com .then. De certa forma, parece mais limpo porque não há aninhamento de callbacks. Com a sintaxe async do ES7, você poderia melhorar ainda mais este exemplo.

Observables

Antes de se decidir por promises, existe algo que surgiu para ajudá-lo a lidar com dados assíncronos chamado Observables.

Vamos analisar o mesmo exemplo escrito com Observables. Neste exemplo, usaremos RxJS para os observables.

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

function addAsync(num1, num2) {
    // usar a API fetch do ES6, que retorna uma promise
    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)
  });

Os Observables podem fazer coisas mais interessantes. Por exemplo, adicionar a função delay de 3 segundos com apenas uma linha de código ou tentar novamente, permitindo que você repita uma chamada um certo número de vezes.

...

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

Você pode ler um dos meus posts sobre RxJs aqui.

Conclusão

Familiarizar-se com callbacks e promises é importante. Entenda-os e use-os. Não se preocupe com Observables por enquanto. Todos os três podem influenciar seu desenvolvimento dependendo da situação.

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