Skip to content

Instantly share code, notes, and snippets.

@ktquez
Forked from wkrueger/promise.md
Created July 4, 2016 12:31
Show Gist options
  • Save ktquez/2f62ace6a4cde3178364a27edf5c21de to your computer and use it in GitHub Desktop.
Save ktquez/2f62ace6a4cde3178364a27edf5c21de to your computer and use it in GitHub Desktop.
Guia Promise

Guia Promises

v. 1.0.

Pode-se afirmar que no momento Promises são a forma mais "padrão" de se tratar com assincronismo no JS. Para quem trabalha com javascript, conhecê-las é essencial. Uma dificuldade comum é que esta API tem uma curva de aprendizado um tanto acentuada de início, especialmente se comparado com as alternativas mais antigas: callbacks e o módulo async. No meu caso, levei ao menos uns 3 meses pra "cair a ficha".

Tentarei aqui fazer uma introdução ao assunto, com foco em passar muitos exemplos. Algumas vezes posso sacrificar a precisão acadêmica propositalmente em nome de uma explicação mais simples. Em alguns casos trago opiniões pessoais.

Apresentação

A principal motivação por trás das Promises é resolver problemas trazidos pelo "pattern" de callbacks. Um desses problemas é a identação excessiva quando de seu uso (A.K.A. callback hell).

Digamos que queremos fazer 3 chamadas subsequentes a serviços REST, cada uma dependendo do resultado da chamada anterior. Exemplo hipotético abaixo. Aqui obtemos uma lista de clientes, e para cada cliente adicionamos de forma assíncrona primeiro cidade, e depois estado -- que depende de cidade. Leia superficialmente, não precisa entender os detalhes.

Sem promise

chamarRest({
    comando : 'listarClientes'
    sucesso : function(clientes) {
    
        clientes.forEach(cliente) {
            chamarRest({
                comando : 'obterCidade' ,
                parametros : { idCliente : cliente.id } ,
                sucesso : function(cidade) {
                    cliente.nomeCidade = cidade.nome
                    chamarRest({
                        comando : 'obterEstado' ,
                        parametros : { codigoIbgeCidade : cidade.codigoIbge }
                        sucesso : function(estado) {
                            cliente.nomeEstado = estado.nome
                            prontos++
                            checaSePronto()
                        }
                    })
                }
            })
        }
        
        var prontos = 0
        function checaSePronto() {
            if (prontos != clientes.length - 1) return
            adicionarNaDOM(clientes)
        }
    }
})

Com promise

var todosClientes = chamarRest({ comando : 'listarClientes' }).then( function(clientes) {
    // transmorma Cliente[] --> Promise<Cliente|Cidade|Estado>[]
    var allRequests = clientes.map( function(cliente) {
        return chamarRest({
            comando : 'obterCidade' ,
            parametros : { idCliente : cliente.id }
        }).then( function(cidade) {
            cliente.nomeCidade = cidade.nome
            return chamarRest({
                comando : 'obterEstado' ,
                parametros : { codigoIbgeCidade : cidade.codigoIbge }
            })
        }).then( function(estado) {
            cliente.nomeEstado = estado.nome
            return cliente
        })
    })
    // transforma Promise<Cliente|Cidade|Estado>[]  --> Promise<(Cliente|Cidade|Estado)[]>
    return Promise.all(allRequests)
})
todosClientes.then( function(clientesComCidadeEstado) {
    adicionarNaDOM(clientesComCidadeEstado)
})

Em suma, o que aconteceu ali em cima:

  • Economizamos 3 níveis de identação (a identação para de crescer para cada chamada subsequente)
  • Não precisamos checar de forma "manual" quando todas as chamadas foram concluídas. Checagem essa que facilmente complica com a estrutura da operação.
  • As chamadas assíncronas possuem entradas pelos parâmetros da função, e saída pelo return; enquanto com callbacks a saída é por um parâmetro de função. Isso propicia o uso de um estilo mais "funcional", com entradas e saídas claras.

Outra vantagem (não tocada ali) é o tratamento de erros facilitado. Veremos isso mais pro final.

Adiante, em diversos momentos vou utilizar o termo "sequência/cadeia de eventos". Isto se refere ao uso subsequente de .then()'s.

EventoInicial().then( function(){
    return outroEvento()
}).then( function() {
    return outroEvento2()
})

Usando

O objeto promise

Um objeto de promise encapsula

  • uma informação qualquer
  • um estado (pendente/concluído/falha)
  • uma ação pra obter essa informação

Você nunca vai acessar os estados e informações diretamente via propriedades, mas sempre pelos métodos .then() e .catch().

construtor

O construtor é usado 99% das vezes pra converter um código de callback/evento em um código de promise. Se o que pretende fazer não é CONVERTER algo entre callback -> promise, então você não deveria estar usando o construtor.

//estilo callback
fazerIsso( 'entrada' , function(resposta) {
    fazerAquilo(resposta, fazerMaisAquilo)
})

//estilo promise
fazerIsso('entrada')
    .then( fazerAquilo )
    .then( fazerMaisAquilo )

No exemplo abaixo convertemos o $.ajax em um formato de Promise (*)

(*) apenas para exemplo hipotético. O jQuery na verdade já expõe uma interface promise-ish nesse método

var ajaxPromise = function(url) {
    return new Promise( function CallbackDoContrutor(fulfill,fail) {
        $.ajax({{
            url : url , 
            success : function(data) {
                return fulfill(data)
            } , 
            error : function(jqXhr, status, error) {
                return fail(error)
            }
        }})
    }
})

ajaxPromise('some-url').then( function(response) {
    fazerCoisas(response)
}).catch( function(err) {
    //exibir erro na tela
})

fulfill e fail recebidos ali pelo CallbackDoConstrutor, são funções a ser chamadas pra indicar que a promessa foi "concluída" ou "falha". Aceitam um (e apenas um) parâmetro o qual é repassado para a próxima etapa da cadeia (o then ou o catch). Tome cuidado pra não chamá-los mais de uma vez. Pessoalmente adoto como prática sempre usá-los junto com return.

(Observação) Observe que o retorno de CallbackDoConstrutor não é utilizado pra nada. Apenas o fulfill e o fail.

Promise.resolve( [valor] )

É exatamente igual a se fazer:

var promiseResolveImitation = function( value ) {
    return new Promise( function(fulfill) {
        fulfill(value)
    })
}
promiseResolveImitation(3).then( function(result) {
    console.log(result) // exibe 3
})
Promise.resolve(3).then( function(result) {
    console.log(result) // exibe 3
})

Promise.reject( [valor] )

A mesma coisa do Promise.resolve, só que com um fail.

NOTA
Uma cadeia de promises SEMPRE nascerá a partir de um construtor, de um Promise.resolve ou de um Promise.reject.

Promise#then( successFn , [failFn] )

(parêntese ilustrativo. favor ignorar se confuso)

class Promise {
    then<K,V>( successFn : (prevResult?) => Promise<K>|K , failFn? : (prevResult?) => Promise<V>|V ) 
        : Promise<K>|Promise<V>
}

É um método do objeto Promise usado pra encadear eventos assíncronos subsequentes.

Caso a etapa anterior tenha sucesso (por exemplo, se fulfill() for chamado), successFn() é executado, caso contrário, roda a failFn().

Os callbacks successFn e failFn recebem como argumento a resposta passada na etapa anterior da sequência de eventos (ex: o parâmetro que foi passado dentro do fulfill() ou do fail())

Promise.resolve('legal').then(function(prev){
    return prev + ' mesmo'
}).then(function(prev) {
    console.log(prev) // legal mesmo
})

UMA PAUSA: Agora que você já sabe que o segundo argumento (failFn) existe, pode ignorá-lo. Você nunca vai usá-lo, isso porque existe o Promise#catch.

Agora vamos ao que importa. O callback successFn é beeem diferente do callback do construtor que vimos lá em cima. Pra repassar dados para as próximas etapas da sequência utilizamos o retorno da função. Se o retorno da função não for uma promise, a próxima etapa é logo chamada de forma síncrona (no mesmo event loop), e o mais importante:

Se o retorno de successFn fror uma Promise, a próxima etapa da cadeia apenas é chamada quando essa Promise tiver sido concluída (seja com sucesso ou falha).

Reiterando, isso é muito importante. É a grande sacada do negócio.

numeroEntreZeroE100.then( function(numero) {
    if (numero < 50) return numero
    return ajaxPromise(ENDERECO + '?numero=' + numero)
}).then( function(resultado) {
    mostraNaTela(resultado)
})

No exemplo acima, temos um bloco que pode ter um resultado síncrono ou assíncrono dependendo do caso.

Promise#catch( fn )

Quando você dá um throw dentro do successFn ou quando o retorno desse é uma Promise falha, o fluxo é encaminhado para o próximo .catch da sequência.

Observe que esse throw não "propagará" para o resto do programa (nem o interromperá), apenas para a sequência de Promise em questão. Em plataformas recentes (com Promise nativa do es6) aparece uma mensagem no console sobre o erro (uncaught rejection), mas em algumas mais antigas ou em bibliotecas um erro pode não deixar aviso nenhum se não tratado com uma etapa .catch() e um log.

Quando há uma falha em uma sequência de Promises, o erro varre a sequência até encontrar o primeiro .catch(). Para que o erro não seja "engolido", não se esqueça de repassá-lo para o resto da cadeia usando throw novamente. Usar return dentro de um .catch() fará o status mudar pra "sucesso" e a sequência cair no .then() seguinte.

Promise.resolve()
    .then( etapa1 )
    .then( etapa2 )  //falhou aqui
    .then( etapa3 )  // pulou este
    .catch( function(err) { // caiu aqui
        console.log(err)
        // não engolir o erro
        throw err
    })
    .then( etapa4 )
    .catch( catch2 )

Se tudo for feito certinho, pra receber um erro e exibí-lo na tela você só precisara de 1 catch pra tripa toda de código. Nada de inúmeras chamadas à função que mostra o erro na tela.

Promise.all( entradas )

(parêntese ilustrativo. favor ignorar se confuso)

class Promise {
    static all( promises : Promise<T>[] ) : Promise<T[]>
}

Recebe Promise[] como entrada.

Promise.all só seguirá para a próxima etapa da cadeia depois que todas as suas entradas finalizarem, ou na primeira falha.

A próxima etapa recebe como resultado um array com as respostas das entradas.

var clientesP = [264,735,335,999 277].map( function(id) {
    return lerDoBanco({
        tabela : 'cliente' ,
        id : id
    })
}) // tipo Promise<Cliente>[]
Promise.all(clientesP).then( function(clientes) {
    // clientes tem tipo Cliente[]
    mostrar(clientes)
}).catch( function(err) {
    mostrarErro(err.message)
    //repassar o erro, caso contrário seguirá a cadeia como se não houvesse erro
    throw err
}) //...

Geralmente você estará utilizando Promise.all e, conjunção com Array#map para criar fluxos "paralelos". (*)

           |
        [1,2,3]
  Array#map + Promise
    /      |      \
 [ p(1),  p(2),   p(3)]
   \       |      /
      Promise.all
           |
    [r(1),r(2),r(3)]

(*) Paralelo entre aspas. Se você entende do event loop sabe do que estou falando.

Padrões, exemplos

Basicão array#map e array#reduce

Eu ainda não sou o guru da programação funcional, mas uma das partes importantes (e mais fáceis) dela é pensar em termos de fluxos de dados e transformações.

Por exemplo, se no início tenho uma lista com 15 ids, e ao final terei 15 , então certamente esta transformação pode ser feita com o map (exceto se esses 15 ids tiverem interdependências/não linearidades).

number[15] => Array#map => Promise<Cliente>[15] => Promise.all => Promise<Cliente[15]> => Promise#then => Cliente[15]

Por outro lado, se de 15 números, ao final quero obter apenas um objeto/estatística, então certamente terei de usar o reduce.

number[15] => Array#reduce => string

Pedidos assíncronos em paralelo

Cria-se um array de promises, depois usa-se o Promise.all para verificar que todas estão prontas e coletar os dados.

Caso a fonte de dados pras promises seja um array, certamente você usará o Array#map.

var clientesP = [264,735,335,999 277].map( function(id) {
    return lerDoBanco({
        tabela : 'cliente' ,
        id : id
    })
}) // tipo Promise<Cliente>[]
Promise.all(clientesP).then( function(clientes) {
    // clientes tem tipo Cliente[]
    mostrar(clientes)
})

Comentário: Em geral você não precisa se preocupar com a quantidade de pedidos (HTTP ou de banco de dados) sendo feitos ao mesmo tempo. As engines por trás disso já cuidam das filas pra você. Por exemplo, se você solicitar que o navegador faça 30 requests HTTP, ele as colocará em uma fila e fará no máximo 6 simultaneamente, conforme pode ser visto no inspetor de rede. É claro que em um cenário ideal o que você vai querer mesmo é não ter que fazer 30 requisições, pois isso é ineficiente. Mas acontece.

Pedidos assíncronos em sequência

Antes

chamada(1)().then( chamada(2) ).then( chamada(3) ).then( chamada(4) )

Equivale a

Promise.resolve().( chamada(1) ).then( chamada(2) ).then( chamada(3) ).then( chamada(4) )

Equivale a

[1,2,3,4].reduce( function(chain,currentItem) {
    return chain.then( chamada(currentItem) )
}, Promise.resolve())

Math, bitches!

Aninhamento

O código em promise tem a característica de poder funcionar com aninhamento. Se fôssemos pensar de forma gráfica, a informação na parte mais profunda da "cascata" parece que vai "subindo" até a raiz, em uma espécie de árvore.

// -- todas as funções personalizadas aqui retornam promises
obterCliente().then( function(cliente) {
    // escopo 1
    return obterCidadeDoCliente(cliente).then( function(cidade) {
        // escopo 2
        cliente.cidade = cidade
        return obterTemplate(cliente).then( function(template) {
            // escopo 3
            return '<div>' + template + '</div>'
        })
    })
}).then( function(templateClienteComDiv) {
    //recebe o resultado do escopo 3
    mostrar(templateClienteComDiv)
})

Otimização

Lembre-se que um dos principais motivos do uso de Promises é cortar as identações. Embora às vezes seja necessário criar mais níveis porque variáveis podem estar fora de escopo, em muitos casos o código pode ser otimizado de modo a cortar identações. Por exemplo, o caso anterior poderia ser simplificado para:

obterCliente().then( function(cliente) {
    //este bloco aqui ainda depende de "cliente" da closure, então não dá pra cortar identação dele
    return obterCidadeDoCliente(cliente).then( function(cidade) {
        cliente.cidade = cidade
        return cliente
    })
})
.then( obterTemplate ) //"escopo 2"
.then( function(template) { //"escopo 3"
    return '<div>' + template + '</div>'
}).then( function(templateClienteComDiv) {
    mostrar(templateClienteComDiv)
})

Se cliente fosse jogado para um escopo acima, poderíamos até ter uma sequência linear. Vai do gosto.

var cliente
obterCliente().then( function(_cliente) {
    cliente = _cliente
    return obterCidadeDoCliente(cliente)
})
.then( function(cidade) { //"escopo 1"
    cliente.cidade = cidade
    return cliente
})
.then( obterTemplate ) //"escopo 2"
.then( function(template) { //"escopo 3"
    return '<div>' + template + '</div>'
}).then( function(templateClienteComDiv) {
    mostrar(templateClienteComDiv)
})

Mostrando erros com 1 catch só

Da mesma forma que em código síncrono podemos usar um try para pegar um erro de um grande bloco de código, em um código de Promise um .catch no final de uma sequência faz o mesmo trabalho.

É importante, porém, que o formato dos erros seja padronizado no programa todo. Ou seja, que os objetos de erro não tenham diversos formatos.

Um exemplo de conversão útil é converter requisições falhadas (status 40x) em promises rejeitadas. Desta forma, uma mensagem de erro vinda do servidor também é incluída no fluxo.

efetuarRequisicao('https://...') // volta HTTP 400 "O cliente solicitado não foi encontrado"
    .then( logicaDoApp ) //pula aqui
    .catch( function mostrarErro(err) {
        popup(err.message, err.code, err.title)
    }) // mostra na tela "O cliente solicitado não foi encontrado"

Tenho um exemplo de um app cujo serviço não usa o padrão REST direito (na vida real todos veremos. Experimente lidar com serviços de governos...). Ao invés disso sempre volta 200, mas com um objeto que pode ter a propriedade erro. Não tem problema.A minha função efetuarRequisicao pode tratar o objeto e subir uma falha caso ele possua um erro.

Fuja do this

Você não precisa do this em código de sua autoria. Nem de classes. Sempre existem outras opções melhores que fazem a mesma coisa. Ele vai te trazer dor de cabeça. Fuja.

Promises também não gostam do this e do pattern de classes.

function Greeter(message) {
    this.greeting = message;
}
Greeter.prototype.greet = function () {
    return "Hello, " + this.greeting;
};

var greeter = new Greeter("world");
Promise.resolve()
    .then( greeter.greet ) // hello, undefined!
    .then(function (resp) {
        console.log(resp);
    });

Solução

    .then( () => greeter.greet() )

Exemplo: Novo registro vs. editar registro

x.controller( ($state, $stateParams) => {
    var tipo = 'cliente'
    var registroP = inicializarAsync(tipo)
    //editar
    if ($stateParams.id) {
        let rec
        registroP = registroP.then( _rec => {
            rec = _rec
            return callREST('fetchRecord', rec.info, $stateParams.id )
        }).then( data => {
            rec.data = data
            return rec
        })
    //novo
    } else {
        registroP = registroP.then( _rec => {
            _rec.data = ALGUM_DADO_INICIAL
            return _rec
        })
    }
    
    //popular $scope
    registroP.then( rec => {
        popularATela($scope, rec)
    }).catch( err => {
        exibirErro(err)
        throw err
    })
    
})

Exemplo: Dados de inicialização

//obter na inicialização
var dadosDoClienteP = obterDadosDoCliente()

document.querySelector('button.mostraDadosCliente').addEventListener('click' , function() {
    mostrar('Aguarde, buscando dados...')
    dadosDoClienteP.then( dados => {
        mostrar(dados)
    })
})

Digamos que o usuário clica no botão e é mostrada a janela. Então ele fecha a janela e clica no botão novamente. Os dados continuarão encapsulados em dadosDoCLienteP e o .then é chamado na hora (sincronamente).

Concluindo

Espero ter ajudado! Abraço!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment