詳細検索

Os Porquês e Como da Promessa em JavaScript

Avatar
por Chen Ziyu

Os Porquês e Como da Promessa em JavaScript
Traduzido do English • Ver original

Se você já fez algum desenvolvimento web após 2015, é provável que já tenha ouvido falar do conceito de Promise. Não seria exagero afirmar que o Promise é onipresente nas bases de código front-end modernas. No entanto, muitos desenvolvedores web — especialmente aqueles que não têm muita experiência em desenvolvimento front-end — têm usado o Promise sem entender profundamente seu funcionamento interno. Isso levou a inúmeros usos indevidos do Promise e consequentes bugs. Neste artigo, vou guiá-lo sobre o que motivou a criação do Promise, como o Promise funciona e como devemos usá-lo em nosso código, partindo do pressuposto de que você não tem muita experiência em desenvolvimento front-end.

Sincronicidade: o começo de tudo

Você ainda se lembra da sua primeira aula de programação? Seu professor provavelmente te ensinou que os computadores executam seu código uma linha após a outra. Eles não avançam para a linha seguinte a menos que tenham terminado a execução da linha à frente. Comumente descrevemos essa forma sequencial de execução de código como síncrona. Certamente tem seus méritos, como simplicidade lógica e legibilidade. No entanto, ela vem com uma limitação significativa: é um desperdício.

Suponha que seu código contenha uma operação que precisa recuperar um dado de outro sistema computacional. A recuperação de dados é demorada, mas não exige que seu código realize nenhum cálculo. Se seu código for síncrono, o sistema que o executa não tem escolha a não ser ficar ocioso enquanto espera a recuperação dos dados terminar, o que inevitavelmente desperdiça tempo e recursos de computação.

Assincronicidade: um substituto mais eficiente em tempo para a sincronicidade

Para remediar a ineficiência de tempo da programação síncrona, os pioneiros do mundo da programação criaram o conceito de assincronia. Ao contrário da sincronicidade, a assíncronia permite que operações demoradas e não intensivas em computação se separem do fluxo principal do programa para abrir espaço para o processamento das operações subsequentes. Uma forma tão nova de programar melhorou enormemente o desempenho do tempo dos programas de computador modernos, especialmente aqueles que envolvem grandes quantidades de comunicação entre sistemas.

Retorno de Chamada: Ainda Precisamos de Sequencialidade na Assincronicidade

Agora que temos o grande conceito de assíncronia, podemos simplesmente deixar todas as operações rodarem em paralelo? A resposta é NÃO, porque existem operações que dependem de outras e não podem começar até a conclusão de suas dependências. Portanto, ainda precisamos encontrar uma forma de permitir que operações ramificadas disparem seus dependentes ao serem concluídas. Em outras palavras, ainda precisamos expressar sequencialidade na programação assíncrona. Em resposta a isso, nossos precursores no mundo da ciência da computação criaram funções de callback. Uma função de callback é uma função que você pode passar para outra função. Nessa outra função, você pode executar a função de callback após ela concluir certas operações. Com a ajuda das funções de callback, você pode reservar com segurança uma operação que consome tempo, sabendo muito bem que suas operações dependentes, especificadas em uma função de callback, começarão após sua conclusão.

A seguir está um exemplo de como você usaria uma função de retorno de chamada. Vamos supor que precisamos recuperar informações do usuário do banco de dados. Depois de recebermos as informações do usuário, precisamos exibi-las em nossa interface de usuário. Podemos descrever a relação sequencial das duas operações acima usando o código abaixo:

function getUserInfo(id, callback) { 
    setTimeout(() => { 
        se (tipo de id === "número") { 
            const userInfo = { 
                id, 
                nome: 'Adam Smith', 
                Idade: 30
            }; 
            callback(userInfo); Chamando a função de callback com as informações do usuário recuperadas
        } else { 
            lançar um novo Erro ("ID Inválido")
        }
    }, 2000); Simulando um atraso de 2 segundos
}

function displayUserInfo(userInfo) { 
    console.log(userInfo)
}

console.log('Obtendo informações do usuário...'); Executado imediatamente
getUserInfo(1, displayUserInfo)

Como podemos ver, 'displayUserInfo' é a função de callback. Passamos para o 'getUserInfo', que primeiro recupera informações do usuário do banco de dados (simulado por 'setTimeout') e então chama a função de callback fornecida com as informações obtidas como argumento. Dessa forma, podemos ter certeza de que nosso programa chamará 'displayUserInfo' apenas depois que as informações do usuário estiverem prontas.

Inferno de Retorno de Chamada: A Armadilha Inescapável das Funções de Retorno

Os callbacks realmente fornecem uma forma de expressarmos relações sequenciais entre operações. No entanto, o uso incorreto deles pode dar origem a uma situação horrenda chamada callback hell. Segundo callbackhell.com, "A causa do callback hell é quando as pessoas tentam escrever JavaScript de forma que a execução ocorra visualmente de cima a baixo." Aqui está um exemplo para ilustrar como isso se manifesta.

função1(função2 () { 
    Algumas operações que consomem muito tempo
    função3(função4 () { 
        Algumas operações que consomem muito tempo
        function5(function6 () { 
            Algumas operações que consomem muito tempo
        })
    })
})

Como você pode ver, quando chamamos 'function1', passamos 'function2' para ele como callback. Após a execução de 'function1' terminar, 'function2' iniciará sua execução. Dentro de 'function2', temos uma situação semelhante em que chamamos 'function3' e passamos 'function4' para ele como callback. O mesmo problema afeta 'function4'. Essa estrutura de código complexa resulta da escrita de callbacks de forma que segue visualmente a sequência de execução do código, e pode piorar ainda mais se mais funções estiverem envolvidas nesse processo. Isso prejudica tanto a legibilidade quanto a manutenibilidade do código.

Prometa ao resgate!

"Existem maneiras de evitar o inferno do callback?", você pode perguntar. Você pode tentar modularizar seu código. No entanto, você ainda não consegue separar totalmente suas funções de callback das funções que as utilizam. Existe uma alternativa mais sofisticada? Permita-me apresentar o Promise! Em JavaScript, um Promise representa a conclusão ou falha eventual de uma operação que consome tempo e seu resultado. Isso nos liberta de ter que encaixar funções umas nas outras e, consequentemente, nos permite produzir código legível e mantido. A seguir, vou demonstrar como o Promise alcança isso.

Promessa 101: Prometo devolver algo, mas não tenho 100% de certeza

Vamos primeiro ver como criar um objeto simples chamado 'Promessa'.

const promise = new Promise((resolve, rejeitar) => { 
    Um pouco de código
    if (isFulfilled) { 
        resolve(resultado)
    } else { 
        rejeitar (erro)
    }
})

Ao construir um objeto 'Promessa', passamos uma função como argumento para o construtor. Essa função por si só recebe duas funções como argumentos. O primeiro argumento é uma função 'resolver'. Chamamos a função 'resolver' com o resultado da operação assíncrona nessa função como argumento para que possamos utilizar esse resultado em operações subsequentes. O segundo argumento é uma função de 'rejeição'. Invocamos ela com uma mensagem de erro como argumento quando a operação assíncrona falhou.

Agora vamos reescrever o exemplo anterior de obter informações do usuário do banco de dados e depois exibir as informações do usuário com Promise.

function getUserInfo(id) { 
    return new Promise((resolve, reject) => { 
        setTimeout(() => { 
            se (tipo de id === "número") { 
                const userInfo = { 
                    id, 
                    nome: 'Adam Smith', 
                    Idade: 30
                }; 
                resolve(userInfo); Resolvendo a Promessa com as informações do usuário recuperadas      
            } else { 
                const error = novo Erro ("ID Inválido")
                rejeitar (erro) // Rejeitar a Promessa com erro
            }
        }, 2000); Simulando um atraso de 2 segundos
    })
}

function displayUserInfo(userInfo) { 
    console.log(userInfo)
}

console.log('Obtendo informações do usuário...'); Executado imediatamente
getUserInfo(1)
    .then((userInfo) => displayUserInfo(userInfo))
    .catch((erro) => console.log(erro))
    .finalmente(() => console.log("Terminado"))

A primeira modificação perceptível que fizemos em 'getUserInfo' é que agora envolvemos o código original em uma função, construímos um novo objeto 'Promise' com a função e retornamos o objeto 'Promise'. Além disso, substituímos a função de callback pela função 'resolve', que nos ajuda a desacoplar 'getUserInfo' das funções de callback. Só precisamos nos preocupar em chamar a função 'resolve' com o que 'getUserInfo' pode oferecer para o próximo passo como argumentos. Não precisamos mais considerar os parâmetros adicionais necessários para invocar funções específicas de callback. Outra mudança notável é que chamamos a função 'rejeitar' em 'getUserInfo' quando há um erro, em vez de lançar o erro diretamente.

Ao chamar 'getUserInfo', não precisamos mais passar uma função de retorno. 'getUserInfo' retorna um objeto 'Promise'. Chamamos o método 'then' no objeto 'Promise' e passamos uma função que queremos chamar após o objeto 'Promise' ser resolvido com sucesso. Essa função usa os mesmos argumentos com os quais chamamos a função 'resolve' em 'getUserInfo'. Quanto ao tratamento de erros, chamamos o método 'catch' no objeto 'Promise' com uma função para lidar com o erro que passamos para a função 'reject' em 'getUserInfo'. No final, a função passada para o método 'finalmente' é executada quando o objeto 'Promise' é resolvido (cumprido ou rejeitado).

O que é poderoso no método 'then' é que você pode chamar o método 'then' várias vezes em um padrão conhecido como cadeia de métodos. Encadear invocações do método 'then' destaca a sequencialidade de todas as operações que você passa para o método 'then', o que torna seu código mais limpo e legível. A seguir está um exemplo.

função multiplyByTwo(input) { 
    return new Promise((resolve, reject) => { 
        se (tipo de entrada === "número") { 
            Saída const = entrada * 2
            resolve(output)
        } else { 
            const error = novo erro ("Entrada Inválida")
            rejeitar (erro)
        }
    })
}

multiplicyByTwo(1)
    .then(input) => multiplyByTwo(input))
    .then(input) => multiplyByTwo(input))
    .then(input) => multiplyByTwo(input))
    .then(input) => multiplyByTwo(input))
    .then((input) => console.log(input))

Como podemos ver, encadeamos cinco invocações de 'multiplyByTwo' usando o método 'then', cada uma das quais (exceto a primeira e a última) recebe a saída da anterior como entrada e passa sua saída para a próxima como entrada. Você consegue adivinhar qual número o exemplo de código acima escreve no console?

Neste blog, abordei por que precisamos usar o Promise na programação assíncrona e como usar o Promisso. Comecei explicando as diferenças entre programação síncrona e assíncrona. Depois, demonstrei o uso de funções de callback na programação assíncrona e suas desvantagens inevitáveis. Por fim, apresentei os benefícios de usar o Promise junto com vários exemplos. Espero que este artigo tenha ajudado você a entender os porquês e comoções do Promise.

Related Articles