const logName = () => { console.log("Han")}setTimeout(logName, 0)console.log("Hi there")
A execução desse código resulta na seguinte saída no console:
// in consoleHi thereHan
Tudo bem. O que está acontecendo?
Acontece que a maneira como fazemos trabalho em JavaScript é usar funções e APIs específicas do ambiente. E isso é uma fonte de grande confusão em JavaScript.
JavaScript sempre é executado em um ambiente.
Freqüentemente, esse ambiente é o navegador. Mas também pode estar no servidor com NodeJS. Mas qual é a diferença?
A diferença – e isso é importante – é que o navegador e o servidor (NodeJS), em termos de funcionalidade, não são equivalentes. Muitas vezes são semelhantes, mas não são iguais.
Vamos ilustrar isso com um exemplo. Digamos que o JavaScript seja o protagonista de um livro de fantasia épica. Apenas um garoto de fazenda comum.
Agora, digamos que esse garoto da fazenda encontrou duas armaduras especiais que deram a eles poderes além dos seus.
Quando eles usaram a armadura do navegador, eles ganharam acesso a um determinado conjunto de recursos.
Quando eles usaram a armadura do servidor, eles ganharam acesso a outro conjunto de recursos.
Esses trajes têm alguma sobreposição, porque seus criadores tinham as mesmas necessidades em certos lugares, mas não em outros.
Isso é o que é um ambiente. Um lugar onde o código é executado, onde existem ferramentas que são construídas sobre a linguagem JavaScript existente. Eles não fazem parte da linguagem, mas a linha costuma ser confusa porque usamos essas ferramentas todos os dias quando escrevemos o código.
setTimeout, buscar, e DOM são todos exemplos de APIs da Web. (Você pode veja a lista completa de APIs da Web aqui.) São ferramentas incorporadas ao navegador e disponibilizadas para nós quando o nosso código é executado.
E como sempre rodamos JavaScript em um ambiente, parece que faz parte da linguagem. Mas eles não são.
Portanto, se você já se perguntou por que pode usar fetch em JavaScript ao executá-lo no navegador (mas precisa instalar um pacote ao executá-lo em NodeJS), esse é o motivo. Alguém achou que fetch era uma boa ideia e o construiu como uma ferramenta para o ambiente NodeJS.
Está confuso? Sim!
Mas agora podemos finalmente entender o que acontece com o trabalho do JavaScript e como ele é contratado.
Acontece que é o ambiente que assume o trabalho, e a maneira de fazer com que o ambiente faça esse trabalho é usar funcionalidades que pertencem ao ambiente. Por exemplo buscar ou setTimeout no ambiente do navegador.
O que acontece com o trabalho?
Ótimo. Portanto, o ambiente assume o trabalho. Então o que?
Em algum momento, você precisará obter os resultados de volta. Mas vamos pensar em como isso funcionaria.
Vamos voltar ao exemplo de malabarismo desde o início. Imagine que você pediu uma nova bola e um amigo começou a jogar a bola em você quando você não estava pronto.
Isso seria um desastre. Talvez você pudesse ter sorte e pegá-lo e colocá-lo em sua rotina de forma eficaz. Mas há uma grande chance de que isso faça com que você deixe cair todas as bolas e estrague sua rotina. Não seria melhor se você desse instruções estritas sobre quando receber a bola?
Acontece que existem regras estritas em torno de quando o JavaScript pode receber trabalho delegado.
Essas regras são regidas pelo loop de eventos e envolvem a fila de microtarefas e macrotarefas. Sim eu conheço. Isso é muito. Mas tenha paciência comigo.
Tudo bem. Portanto, quando delegamos código assíncrono ao navegador, o navegador pega e executa o código e assume essa carga de trabalho. Mas pode haver várias tarefas atribuídas ao navegador, portanto, precisamos ter certeza de que podemos priorizar essas tarefas.
É aqui que a fila de microtarefa e a fila de macrotarefa entram em ação. O navegador pegará o trabalho, fará e, em seguida, colocará o resultado em uma das duas filas com base no tipo de trabalho que recebe.
As promessas, por exemplo, são colocadas na fila de microtarefa e têm uma prioridade mais alta.
Eventos e setTimeout são exemplos de trabalho que são colocados na fila de macrotask e têm uma prioridade mais baixa.
Agora, uma vez que o trabalho é concluído e colocado em uma das duas filas, o loop de eventos será executado para frente e para trás e verificará se o JavaScript está ou não pronto para receber os resultados.
Somente quando o JavaScript terminar de executar todo o seu código síncrono e estiver bom e pronto, o loop de eventos começará a ser selecionado nas filas e a devolver as funções ao JavaScript para execução.
Então, vamos dar uma olhada em um exemplo:
setTimeout(() => console.log("hello"), 0) fetch("https://someapi/data").then(response => response.json()) .then(data => console.log(data))console.log("What soup?")
Qual será o pedido aqui?
- Em primeiro lugar, setTimeout é delegado ao navegador, que faz o trabalho e coloca a função resultante na fila de macrotask.
- Em segundo lugar, a busca é delegada ao navegador, que faz o trabalho. Ele recupera os dados do terminal e coloca as funções resultantes na fila de microtarefa.
- Javascript desconecta “Que sopa”?
- O loop de eventos verifica se o JavaScript está pronto ou não para receber os resultados do trabalho enfileirado.
- Quando o console.log é concluído, o JavaScript está pronto. O loop de evento seleciona funções enfileiradas da fila de microtarefa, que tem uma prioridade mais alta, e as devolve ao JavaScript para execução.
- Depois que a fila de microtarefa está vazia, o retorno de chamada setTimeout é retirado da fila de macrotarefa e devolvido ao JavaScript para execução.
In console:// What soup?// the data from the api// hello
Promessas
Agora você deve ter um bom conhecimento sobre como o código assíncrono é tratado pelo JavaScript e pelo ambiente do navegador. Então, vamos falar sobre promessas.
Uma promessa é uma construção JavaScript que representa um valor futuro desconhecido. Conceitualmente, uma promessa é apenas o JavaScript prometendo retornar um valor. Pode ser o resultado de uma chamada de API ou um objeto de erro de uma solicitação de rede com falha. Você tem a garantia de obter algo.
const promise = new Promise((resolve, reject) => { // Make a network request if (response.status === 200) { resolve(response.body) } else { const error = { ... } reject(error) }})promise.then(res => { console.log(res)}).catch(err => { console.log(err)})
Uma promessa pode ter os seguintes estados:
- cumprida – ação concluída com sucesso
- rejeitado – ação falhou
- pendente – nenhuma ação foi concluída
- liquidado – foi cumprido ou rejeitado
Uma promessa recebe uma função de resolução e rejeição que pode ser chamada para acionar um desses estados.
Um dos grandes argumentos de venda das promessas é que podemos encadear funções que queremos que aconteçam em caso de sucesso (resolução) ou falha (rejeição):
- Para registrar uma função para ser executada com sucesso, usamos. Então
- Para registrar uma função para executar em caso de falha, usamos .catch
// Fetch returns a promisefetch("https://swapi.dev/api/people/1") .then((res) => console.log("This function is run when the request succeeds", res) .catch(err => console.log("This function is run when the request fails", err) // Chaining multiple functions fetch("https://swapi.dev/api/people/1") .then((res) => doSomethingWithResult(res)) .then((finalResult) => console.log(finalResult)) .catch((err => doSomethingWithErr(err))
Perfeito. Agora vamos dar uma olhada mais de perto em como isso se parece sob o capô, usando fetch como exemplo:
const fetch = (url, options) => { // simplified return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest() // ... make request xhr.onload = () => { const options = { status: xhr.status, statusText: xhr.statusText ... } resolve(new Response(xhr.response, options)) } xhr.onerror = () => { reject(new TypeError("Request failed")) }} fetch("https://swapi.dev/api/people/1") // Register handleResponse to run when promise resolves .then(handleResponse) .catch(handleError) // conceptually, the promise looks like this now: // { status: "pending", onsuccess: [handleResponse], onfailure: [handleError] } const handleResponse = (response) => { // handleResponse will automatically receive the response, ¨ // because the promise resolves with a value and automatically injects into the function console.log(response) } const handleError = (response) => { // handleError will automatically receive the error, ¨ // because the promise resolves with a value and automatically injects into the function console.log(response) } // the promise will either resolve or reject causing it to run all of the registered functions in the respective arrays// injecting the value. Let's inspect the happy path: // 1. XHR event listener fires// 2. If the request was successfull, the onload event listener triggers// 3. The onload fires the resolve(VALUE) function with given value// 4. Resolve triggers and schedules the functions registered with .then
Portanto, podemos usar promessas para fazer trabalho assíncrono e ter certeza de que podemos lidar com qualquer resultado dessas promessas. Essa é a proposta de valor. Se você quiser saber mais sobre as promessas, pode ler mais sobre elas aqui e aqui.
Quando usamos promessas, encadeamos nossas funções na promessa de lidar com os diferentes cenários.
Isso funciona, mas ainda precisamos lidar com nossa lógica dentro de callbacks (funções aninhadas), uma vez que recebemos nossos resultados de volta. E se pudéssemos usar promessas, mas escrever um código de aparência síncrona? Acontece que podemos.
Async / Await
Async / Await é uma forma de escrever promessas que nos permite escrever código assíncrono de forma síncrona. Vamos dar uma olhada.
const getData = async () => { const response = await fetch("https://jsonplaceholder.typicode.com/todos/1") const data = await response.json() console.log(data)}getData()
Nada mudou sob o capô aqui. Ainda estamos usando promessas para buscar dados, mas agora parece síncrono e não temos mais os blocos .then e .catch.
Async / Await é, na verdade, apenas um açúcar sintático, fornecendo uma maneira de criar código mais fácil de raciocinar, sem alterar a dinâmica subjacente.
Vamos dar uma olhada em como funciona.
Async / Await nos permite usar geradores para pausa a execução de uma função. Quando estamos usando async / await não estamos bloqueando porque a função está devolvendo o controle ao programa principal.
Então, quando a promessa é resolvida, estamos usando o gerador para devolver o controle à função assíncrona com o valor da promessa resolvida.
Você pode ler mais aqui para uma ótima visão geral de geradores e código assíncrono.
Na verdade, agora podemos escrever código assíncrono que se parece com código síncrono. O que significa que é mais fácil raciocinar e podemos usar ferramentas síncronas para tratamento de erros, como try / catch:
const getData = async () => { try { const response = await fetch("https://jsonplaceholder.typicode.com/todos/1") const data = await response.json() console.log(data) } catch (err) { console.log(err) } }getData()
Tudo bem. Então, como o usamos? Para usar async / await, precisamos preceder a função com async. Isso não a torna uma função assíncrona, apenas nos permite usar o await dentro dela.
Deixar de fornecer a palavra-chave async resultará em um erro de sintaxe ao tentar usar o await dentro de uma função regular.
const getData = async () => { console.log("We can use await in this function")}
Por causa disso, não podemos usar async / await no código de nível superior. Mas async e await ainda são apenas açúcar sintático em vez de promessas. Portanto, podemos lidar com casos de nível superior com encadeamento de promessa:
async function getData() { let response = await fetch('http://apiurl.com');}// getData is a promisegetData().then(res => console.log(res)).catch(err => console.log(err);
Isso expõe outro fato interessante sobre async / await. Ao definir uma função como assíncrona, sempre retornará uma promessa.
Usar async / await pode parecer mágica à primeira vista. Mas, como qualquer mágica, é apenas uma tecnologia suficientemente avançada que evoluiu ao longo dos anos. Esperamos que agora você tenha um conhecimento sólido dos fundamentos e possa usar async / await com confiança.
Se você veio aqui, parabéns. Você acabou de adicionar à sua caixa de ferramentas um conhecimento importante sobre JavaScript e como ele funciona com seus ambientes.
Este é definitivamente um assunto confuso e as linhas nem sempre são claras. Mas agora você espera ter uma compreensão de como o JavaScript funciona com código assíncrono no navegador e um domínio mais forte sobre promessas e assíncrono / aguardar.
Se você gostou deste artigo, também pode gostar do meu canal do Youtube. Atualmente tenho um série de fundamentos da web indo por onde eu passo HTTP, construindo servidores web do zero e mais.
Também há uma série acontecendo construir um aplicativo inteiro com React, se essa for a sua geléia. E pretendo adicionar muito mais conteúdo aqui no futuro, aprofundando os tópicos de JavaScript.
E se você quiser dizer oi ou bater um papo sobre desenvolvimento web, você pode sempre entrar em contato comigo no twitter em @foseberg. Obrigado por ler!