O presente artigo apresenta uma (não tão) breve introdução sobre JavaScript Promises (promessas, em tradução livre), estruturas que facilitam a execução assíncrona de elementos encadeados. Inclui, também, um caso de uso para consulta assíncrona de produtos com preços, apresentando um feedback ao usuário durante a execução.

Introdução

Uma Promise é um objeto que manipula valores de forma assíncrona e que não são necessariamente reconhecidos no momento de sua criação (MOZILLA 2017). Basicamente, uma Promise representa um bloco de código que pode não finalizar a sua execução no momento em que é criado, sendo paralelizado prontamente, evitando, assim, o bloqueio do fluxo de execução principal.

O exemplo abaixo apresenta a execução assíncrona de um cálculo qualquer, através de Promises.

/**
 * Executa um Cálculo Qualquer
 * @type Promise
 */
var calcSomethingPromise = new Promise(function (resolve, reject) {
    var counter = 0;
    var limit   = Math.pow(10, 10);

    for (var i = 0; i <= limit; i++) {
        counter = counter + 1;
    }

    resolve(counter);
});

Nota-se que um objeto do tipo Promise foi inicializado através de seu construtor, recebendo como parâmetro uma closure que possui dois parâmetros: resolve e reject. Ambos parâmetros também são do tipo closure, onde resolve deve ser utilizado quando há sucesso de execução, e reject quando um erro for encontrado.

Execução

Quando uma Promise é construída, ela automaticamente inicia a sua execução de forma assíncrona. Uma Promise pode estar em três estados distintos, dependendo da sua execução atual:

  • pending (pendente): onde esta ainda não foi resolvida ou rejeitada;
  • fullfilled (realizada): a execução foi finalizada com sucesso através da closure resolve;
  • rejected (rejeitada): seu processamento foi rejeitado com a closure reject.

O estado de uma Promise não pode ser acessado. Todavia, deve-se utilizar os métodos then e catch para encadeamento de outras closures. Estes métodos também retornam novas Promises, caracterizando o padrão de projeto fluent interface.

// Cálculo Qualquer
calcSomethingPromise
    // Sucesso 1: Cria Objeto Totalizador
    .then(function (counter) {
        return {'total': counter};
    })
    // Sucesso 2: Apresenta Informações no Console
    .then(function (element) {
        console.info(element);
    })
    // Erro: Impossível Continuar
    .catch(function (message) {
        console.error(message);
    });

O exemplo anterior adiciona três closures de execução. A primeira closure configurada com o método then, recebe como parâmetro o resultado de contabilização informado em resolve, interno à estrutura da Promise. Já a segunda closure do método then, recebe como parâmetro o resultado da primeira, um objeto com o atributo total, apresentando no console o elemento construído anteriormente. Por fim, a terceira closure pelo método catch, contém o tratamento de erro caso reject seja executado.

Constata-se que as closures configuradas pelo método then são executadas posteriormente ao cálculo e caso algum erro seja apresentado, o tratamento pode ser aplicado no método catch. Ainda, caso um destes fluxos de execução apresente uma exceção através de um throw, o método catch é invocado, recebendo como parâmetro a mensagem de erro.

Caso de Uso

A criação de camadas de responsabilidade no código-fonte pode usufruir da estrutura de Promises através do padrão de projeto Repository. O exemplo abaixo apresenta uma camada de serviço responsável por apresentar uma lista de produtos com seus respectivos preços. Por sua vez, a camada de serviço acessa duas camadas de repositório, onde a primeira consulta os produtos filtrados, e a segunda, os preços.

/**
 * Camada de Repositório de Produtos
 */
var ProductsRepository = function () {
    /**
     * Consulta de Produtos
     *
     * @param  object  params Parâmetros de Execução
     * @return Promise Execução Assíncrona
     */
    this.fetch = function (params) {
        // Execução Assíncrona
        return new Promise(function (resolve, reject) {
            // Consulta Servidor
            var handler = $.get('/ws/products', params);

            // Sucesso!
            handler.done(function (data) {
                // Dados Encontrados
                // Exemplo de Estrutura:
                // [
                //     {"id": 1, "name": "Product A"},
                //     {"id": 2, "name": "Product B"}
                // ]
                resolve(data);
            });

            // Erro Encontrado
            handler.fail(function (error) {
                // Apresentar Erro Encontrado
                reject(error);
            });
        });
    };
};
/**
 * Camada de Serviço de Produtos
 */
var ProductsService = function () {
    /**
     * Camada de Repositório de Produtos
     * @type ProductsRepository
     */
    var productsRepository = new ProductsRepository();

    /**
     * Camada de Repositório de Preços de Produtos
     *
     * Esta camada possui a mesma estrutura daquela utilizada no Repositório de
     * Produtos, usufruindo de requisições AJAX por jQuery. Porém, captura
     * informações do Web Service de preços ao invés de produtos.
     *
     * @type PricesRepository
     */
    var pricesRepository = new PricesRepository();

    /**
     * Consulta de Produtos e Preços
     *
     * @param  object  params Parâmetros de Execução
     * @return Promise Execução Assíncrona
     */
    this.fetch = function (params) {
        // Capturar Produtos
        return productsRepository.fetch(params).then(function (products) {
            // Mapear Produtos
            var dataset = products.reduce(function (dataset, datum) {
                dataset[datum.id] = datum;
                return dataset;
            }, {});
            // Capturar Preços de Produtos
            return pricesRepository.fetch(params).then(function (prices) {
                // Configurar Preços dos Produtos
                prices.forEach(function (datum) {
                    dataset[datum.id].price = datum.price;
                });
                // Remover Mapeamento
                return Object.keys(dataset).map(function (id) {
                    return dataset[id];
                });
            });
        });
    };
};

O método ProductsService::fetch acessa duas camadas de repositório, ProductsRepository e PricesRepository, capturando primeiramente os produtos e, após, os preços destes produtos, através de seus métodos fetch. Ambos os métodos das camadas de repositório retornam objetos do tipo Promise e são, portanto, assíncronos.

Ainda, o método ProductsService::fetch retorna um objeto do tipo Promise, tendo em vista que há o retorno do resultado do método Promise::then. Os métodos then e catch de objetos do tipo Promise retornam outras Promises, trabalhando como fluent interfaces e possibilitando encadeamento.

Quando a Promise retornada pelo método ProductsRepository::fetch finalizar a sua execução através da closure resolve, o método then receberá como parâmetro os produtos encontrados na camada de repositório. No exemplo acima, há um mapeamento dos produtos apresentados, criando uma estrutura de hashmap, facilitando a captura posterior dos objetos encontrados.

Após o mapeamento, define-se uma nova consulta, desta vez ao repositório de preços, através do método PricesRepository::fetch. Este método retorna um novo objeto do tipo Promise; o método then é invocado e a closure passada recebe como parâmetro um conjunto de preços de produtos encontrados no Web Service.

Para cada produto encontrado anteriormente, configura-se o seu preço. Por fim, a última closure retorna um array com todos os objetos configurados, sem o mapeamento de hashes, limpando a estrutura criada durante indexação.

Encadeamento de Promessas

O exemplo anterior apresenta uma característica interessante de elementos do tipo Promise: para cada closure informada pelo método then, o seu retorno será apresentado como parâmetro para o próximo then.

Se uma closure apresentar um tipo básico, como number ou string, este será diretamente informado como parâmetro na closure do próximo then encadeado. Caso o retorno seja um outro objeto do tipo Promise, o próprio JavaScript efetuará sua execução, de forma assíncrona, seguindo o fluxo de processamento.

Portanto, verifica-se que o retorno de uma closure informada nos métodos then é importante, pois possibilita o encadeamento com reutilização dos resultados encontrados nas execuções assíncronas. Caso uma closure não informe um retorno, a próxima closure encadeada através do método then receberá como parâmetro um valor undefined.

Consulta

O exemplo anterior define uma camada de serviço ProductsService que possui um método fetch responsável pela captura de produtos e seus preços. A forma com que estes dados são capturados não é apresentada pelo método, porém, sabe-se que este retorna um objeto do tipo Promise.

Com base nestas informações, o próximo exemplo apresenta a sua utilização, adicionando um evento ao botão de pesquisa e efetuando a consulta dos produtos com base em alguns filtros definidos.

/**
 * Botão de Pesquisa
 * @type jQuery.fn
 */
var btnSearch = $('#btn-search');

/**
 * Tipo de Produto
 * @type jQuery.fn
 */
var formType = $('#form-type');

/**
 * Elemento de "Carregando"
 * @type jQuery.fn
 */
var elLoading = $('#el-loading');

/**
 * Camada de Serviço de Produtos
 * @type ProductsService
 */
var productsService = new ProductsService();

/**
 * Renderiza Produtos
 *
 * @param  Array products Elementos para Renderização
 * @return self  Próprio Objeto para Encadeamento
 */
var renderCallback = function (products) {
    // TODO Renderizar Produtos
    return this;
};

// Evento: Filtrar
btnSearch.on('click', function () {
    // Inicialização
    var params = {};
    // Parâmetro: Tipo de Produto
    params.type = formType.val();
    // Exibir "Carregando"
    elLoading.show();
    // Consultar
    productsService.fetch(params).then(function (products) {
        // Renderizar Produtos
        renderCallback(products);
        // Esconder "Carregando"
        elLoading.hide();
    }).catch(function (error) {
        // Renderizar Vazio
        renderCallback([]);
        // Exibir Erro
        console.error(error);
        // Esconder "Carregando"
        elLoading.hide();
    });
});

O código anterior captura o botão com identificador btn-search, registrando um callback para eventos do tipo onClick. Caso este evento seja executado, há uma inicialização dos parâmetros de consulta, configurando o atributo type com o valor do campo de formulário com identificador form-type.

Antes de inicializar a consulta de produtos, apresenta-se um feedback ao usuário, exibindo uma animação de loading. Após, há uma consulta de produtos através do método ProductsService::fetch que retorna um objeto do tipo Promise. A seguir, adiciona-se uma closure encadeada através do método then com parâmetro único: um array de produtos com seus nomes e preços, resultantes da pesquisa assíncrona interna.

Com a execução finalizada com sucesso, há a renderização de produtos e a omissão do feedback de loading. Caso qualquer erro seja encontrado, define-se uma outra closure no método catch, responsável pela limpeza da renderização de produtos e apresentação de erros no Console.

Conclusão

Observa-se que durante a utilização de objetos da classe Promise, há uma melhora a construção e definição de sequência de execuções assíncronas no código-fonte, inibindo a criação de programas classificados como Callback Hell (ODGEN 2012).

Nota-se, também, que o encapsulamento de funcionalidades que dependem de execuções assíncronas, como consultas a banco de dados ou Web Services, tornam-se transparentes: a programação de recursos que possuem dependências destas funções são praticamente definidas no formato síncrono, deixando a cargo da linguagem de programação o tratamento do paralelismo.

Posteriores estudos deverão apresentar como efetuar tratamento de erros com objetos do tipo Promise, através do método catch. Também há possibilidade da criação de um documento que descreve a utilização do banco de dados PouchDB, onde sua API homônima é totalmente definida com Promises.

Referências

Veja Mais