Hoje, na tradução da sétima parte do manual do Node.js., falaremos sobre programação assíncrona, consideraremos questões como o uso de retornos de chamada, promessas e a construção assíncrona / aguardada, além de discutir o trabalho com eventos.

[Aconselhamos a ler] Outras partes do cicloParte 1:
Informações gerais e introduçãoParte 2:
JavaScript, V8, alguns truques de desenvolvimentoParte 3:
Hospedagem, REPL, trabalho com o console, módulosParte 4:
arquivos npm, package.json e package-lock.jsonParte 5:
npm e npxParte 6:
loop de eventos, pilha de chamadas, temporizadoresParte 7:
Programação assíncronaParte 8:
Guia Node.js, Parte 8: Protocolos HTTP e WebSocketParte 9:
Guia Node.js, parte 9: trabalhando com o sistema de arquivosParte 10:
Guia do Node.js, Parte 10: Módulos padrão, fluxos, bancos de dados, NODE_ENVPDF completo do Guia Node.js. Assincronia em linguagens de programação
O próprio JavaScript é uma linguagem de programação síncrona e de thread único. Isso significa que você não pode criar novos threads no código que são executados em paralelo. No entanto, os computadores são inerentemente assíncronos. Ou seja, certas ações podem ser executadas independentemente do fluxo de execução do programa principal. Nos computadores modernos, cada programa recebe uma certa quantidade de tempo do processador; quando esse tempo acaba, o sistema fornece recursos para outro programa, também por um tempo. Tais comutações são realizadas ciclicamente, são feitas tão rapidamente que uma pessoa simplesmente não consegue perceber, como resultado, pensamos que nossos computadores executam muitos programas simultaneamente. Mas isso é uma ilusão (para não mencionar máquinas multiprocessadoras).
Nas entranhas dos programas são utilizadas interrupções - sinais transmitidos ao processador e permitindo atrair a atenção do sistema. Não entraremos em detalhes, o mais importante é lembrar que o comportamento assíncrono, quando o programa é pausado até o momento em que precisa de recursos do processador, é completamente normal. No momento em que o programa não carrega o sistema com trabalho, o computador pode resolver outros problemas. Por exemplo, com essa abordagem, quando um programa está aguardando uma resposta a uma solicitação de rede feita a ele, ele não bloqueia o processador até que uma resposta seja recebida.
Como regra, as linguagens de programação são assíncronas, algumas delas dão ao programador a capacidade de controlar mecanismos assíncronos, usando as ferramentas de linguagem incorporadas ou as bibliotecas especializadas. Estamos falando de linguagens como C, Java, C #, PHP, Go, Ruby, Swift, Python. Alguns deles permitem programar em estilo assíncrono, usando threads, iniciando novos processos.
Assincronia do JavaScript
Como já mencionado, o JavaScript é uma linguagem síncrona de thread único. As linhas de código escritas em JS são executadas na ordem em que aparecem no texto, uma após a outra. Por exemplo, aqui está um programa JS muito normal que demonstra esse comportamento:
const a = 1 const b = 2 const c = a * b console.log(c) doSomething()
Mas o JavaScript foi criado para uso em navegadores. Sua principal tarefa, desde o início, era organizar o processamento de eventos relacionados às atividades do usuário. Por exemplo, são eventos como
onClick
,
onMouseOver
,
onChange
,
onSubmit
e assim por diante. Como resolver esses problemas dentro da estrutura de um modelo de programação síncrona?
A resposta está no ambiente em que o JavaScript é executado. Nomeadamente, o navegador permite resolver efetivamente esses problemas, fornecendo ao programador as APIs apropriadas.
No ambiente do Node.js, existem ferramentas para executar operações de E / S sem bloqueio, como trabalhar com arquivos, organizar a troca de dados em uma rede e assim por diante.
Retornos de chamada
Se falamos de JavaScript baseado em navegador, pode-se notar que é impossível saber com antecedência quando o usuário clica em um botão. Para garantir que o sistema responda a esse evento, um manipulador é criado para ele.
O manipulador de eventos aceita uma função que será chamada quando o evento ocorrer. É assim:
document.getElementById('button').addEventListener('click', () => { // })
Essas funções também são chamadas de funções de retorno de chamada ou retornos de chamada.
Um retorno de chamada é uma função regular que é passada como um valor para outra função. Ele será chamado apenas quando um determinado evento ocorrer. JavaScript implementa o conceito de funções de primeira classe. Tais funções podem ser atribuídas a variáveis e passadas para outras funções (chamadas funções de ordem superior).
No desenvolvimento JavaScript do lado do cliente, a abordagem é difundida quando todo o código do cliente é agrupado em um ouvinte do evento
load
de um objeto de
window
, que chama o retorno de chamada passado para ele depois que a página está pronta para o trabalho:
window.addEventListener('load', () => { // // })
Os retornos de chamada são usados em todos os lugares, e não apenas para manipular eventos DOM. Por exemplo, já conhecemos seu uso em temporizadores:
setTimeout(() => {
As solicitações XHR também usam retornos de chamada. Nesse caso, parece atribuir uma função à propriedade correspondente. Uma função semelhante será chamada quando um determinado evento ocorrer. No exemplo a seguir, esse evento é uma alteração do estado da solicitação:
const xhr = new XMLHttpRequest() xhr.onreadystatechange = () => { if (xhr.readyState === 4) { xhr.status === 200 ? console.log(xhr.responseText) : console.error('error') } } xhr.open('GET', 'https://yoursite.com') xhr.send()
Handling Tratamento de erros em retornos de chamada
Vamos falar sobre como lidar com erros nos retornos de chamada. Há uma estratégia comum para lidar com esses erros, que também é usada no Node.js. Consiste no fato de que o primeiro parâmetro de qualquer função de retorno de chamada é um objeto de erro. Se não houver erros,
null
será gravado neste parâmetro. Caso contrário, haverá um objeto de erro contendo sua descrição e informações adicionais sobre ele. Aqui está o que parece:
fs.readFile('/file.json', (err, data) => { if (err !== null) { // console.log(err) return } // , console.log(data) })
▍ Problema de retorno de chamada
É conveniente usar retornos de chamada em situações simples. No entanto, cada retorno de chamada é um nível adicional de aninhamento de código. Se vários retornos de chamada aninhados são usados, isso rapidamente leva a uma complicação significativa da estrutura do código:
window.addEventListener('load', () => { document.getElementById('button').addEventListener('click', () => { setTimeout(() => { items.forEach(item => { //, - }) }, 2000) }) })
Neste exemplo, apenas 4 níveis de código são mostrados, mas, na prática, é possível encontrar um grande número de níveis, geralmente chamado de "inferno de retorno de chamada". Você pode lidar com esse problema usando outras construções de idioma.
Promessas e assíncronas / aguardam
Começando com o padrão ES6, o JavaScript apresenta novos recursos que facilitam a gravação de código assíncrono, eliminando a necessidade de retornos de chamada. Estamos falando das promessas que apareceram no ES6 e da construção assíncrona / aguardada que apareceu no ES8.
▍ Promessas
Promessas (objetos de promessa) são uma das maneiras de trabalhar com construções de software assíncronas em JavaScript, o que, em geral, reduz o uso de retornos de chamada.
Familiaridade com promessas
As promessas são geralmente definidas como objetos proxy para determinados valores, cuja aparência é esperada no futuro. As promessas também são chamadas de "promessas" ou "resultados prometidos". Embora esse conceito exista há muitos anos, as promessas foram padronizadas e adicionadas ao idioma apenas no ES2015. No ES2017, o design assíncrono / aguardado, baseado em promessas e que pode ser considerado como uma substituição conveniente, apareceu. Portanto, mesmo se você não planeja usar promessas regulares, é importante entender como elas funcionam para o uso efetivo da construção assíncrona / aguardada.
Como as promessas funcionam
Depois que uma promessa é chamada, ela entra em um estado pendente. Isso significa que a função que causou a promessa continua sendo executada, enquanto alguns cálculos são realizados na promessa, após o que a promessa informa sobre ela. Se a operação executada pela promessa for concluída com êxito, a promessa será transferida para o estado cumprido. Diz-se que essa promessa foi resolvida com sucesso. Se a operação for concluída com um erro, a promessa será colocada no estado rejeitado.
Vamos falar sobre o trabalho com promessas.
Criar promessas
A API para trabalhar com promessas nos fornece o construtor correspondente, chamado por um comando no formato
new Promise()
. Veja como as promessas são criadas:
let done = true const isItDoneYet = new Promise( (resolve, reject) => { if (done) { const workDone = 'Here is the thing I built' resolve(workDone) } else { const why = 'Still working on something else' reject(why) } } )
O Promis verifica a constante global
done
e, se seu valor for
true
, é resolvido com sucesso. Caso contrário, a promessa é rejeitada. Usando os parâmetros de
resolve
e
reject
, que são funções, podemos retornar valores da promessa. Nesse caso, retornamos uma string, mas aqui um objeto pode ser usado.
Trabalhar com promessas
Criamos uma promessa acima, agora considere trabalhar com ela. É assim:
const isItDoneYet = new Promise( //... ) const checkIfItsDone = () => { isItDoneYet .then((ok) => { console.log(ok) }) .catch((err) => { console.error(err) }) } checkIfItsDone()
Chamar
checkIfItsDone()
levará à execução da
isItDoneYet()
isItDoneYet
isItDoneYet()
e à organização de aguardar sua resolução. Se a promessa for resolvida com êxito, o retorno de chamada passado para o método
.then()
funcionará. Se ocorrer um erro, ou seja, a promessa será rejeitada, ela poderá ser processada na função passada para o método
.catch()
.
Promessas de encadeamento
Os métodos Promise retornam promessas, o que permite combiná-las em cadeias. Um bom exemplo desse comportamento é a
busca de API baseada no navegador, que é uma camada de abstração sobre
XMLHttpRequest
. Existe um pacote npm bastante popular para o Node.js que implementa a API de busca, que discutiremos mais adiante. Essa API pode ser usada para carregar certos recursos de rede e, graças à possibilidade de combinar promessas em cadeias, para organizar o processamento subsequente dos dados baixados. De fato, quando você chama a API Fetch por meio de uma chamada para a função
fetch()
, uma promessa é criada.
Considere o seguinte exemplo de promessas de encadeamento:
const fetch = require('node-fetch') const status = (response) => { if (response.status >= 200 && response.status < 300) { return Promise.resolve(response) } return Promise.reject(new Error(response.statusText)) } const json = (response) => response.json() fetch('https://jsonplaceholder.typicode.com/todos') .then(status) .then(json) .then((data) => { console.log('Request succeeded with JSON response', data) }) .catch((error) => { console.log('Request failed', error) })
Aqui, usamos o pacote npm
node-fetch e o recurso
jsonplaceholder.typicode.com como uma fonte de dados JSON.
Neste exemplo, a função
fetch()
é usada para carregar um item da lista TODO usando uma cadeia de promessas. Após executar
fetch()
,
é retornada uma
resposta que possui muitas propriedades, dentre as quais estamos interessados no seguinte:
status
é um valor numérico que representa o código de status HTTP.statusText
- uma descrição textual do código de status HTTP, representado pela sequência OK
se a solicitação foi bem-sucedida.
O objeto de
response
possui um método
json()
que retorna uma promessa, após a resolução da qual o conteúdo processado do corpo da solicitação é apresentado, apresentado no formato JSON.
Dado o exposto, descrevemos o que está acontecendo neste código. A primeira promessa na cadeia é representada pela função
status()
que anunciamos, que verifica o status da resposta e, se indica que a solicitação falhou (ou seja, o código de status HTTP não está no intervalo entre 200 e 299), a promessa é rejeitada. Essa operação leva ao fato de que outras expressões
.then()
na cadeia de promessas não são executadas e chegamos imediatamente ao método
.catch()
, emitindo para o console, junto com a mensagem de erro, o texto
Request failed
.
Se o código de status HTTP nos convém, a função
json()
declarada por nós é chamada. Como a promessa anterior, se resolvida com êxito, retorna um objeto de
response
, nós o usamos como um valor de entrada para a segunda promessa.
Nesse caso, retornamos os dados JSON processados, para que a terceira promessa os receba, após o que eles, precedidos de uma mensagem de que, como resultado da solicitação, foi possível obter os dados necessários, são exibidos no console.
Tratamento de erros
No exemplo anterior, tínhamos um método
.catch()
anexado a uma cadeia de promessas. Se algo na cadeia de promessas der errado e ocorrer um erro, ou se uma das promessas for rejeitada, o controle será transferido para a expressão mais próxima
.catch()
. Aqui está a situação em que ocorre um erro em uma promessa:
new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { console.error(err) })
Aqui está um exemplo de acionar
.catch()
após rejeitar uma promessa:
new Promise((resolve, reject) => { reject('Error') }) .catch((err) => { console.error(err) })
Tratamento de erros em cascata
E se ocorrer um erro na expressão
.catch()
? Para lidar com esse erro, você pode incluir outra expressão
.catch()
na cadeia de promessas (e, em seguida, anexar tantas
.catch()
à cadeia conforme necessário):
new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { throw new Error('Error') }) .catch((err) => { console.error(err) })
Agora, vamos ver alguns métodos úteis usados para gerenciar promessas.
Promise.all ()
Se você precisar executar alguma ação após resolver várias promessas, poderá fazer isso usando o comando
Promise.all()
. Considere um exemplo:
const f1 = fetch('https://jsonplaceholder.typicode.com/todos/1') const f2 = fetch('https://jsonplaceholder.typicode.com/todos/2') Promise.all([f1, f2]).then((res) => { console.log('Array of results', res) }) .catch((err) => { console.error(err) })
No ES2015, a sintaxe da atribuição destrutiva apareceu; usando-a, você pode criar construções da seguinte forma:
Promise.all([f1, f2]).then(([res1, res2]) => { console.log('Results', res1, res2) })
Aqui, como exemplo, consideramos a API Fetch, mas
Promise.all()
, é claro, permite que você trabalhe com todas as promessas.
Promise.race ()
O comando
Promise.race()
permite executar a ação especificada depois que uma das promessas passadas a ela for resolvida. O retorno de chamada correspondente que contém os resultados desta primeira promessa é chamado apenas uma vez. Considere um exemplo:
const first = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'first') }) const second = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'second') }) Promise.race([first, second]).then((result) => { console.log(result)
Erro TypeError não detectado que ocorre ao trabalhar com promessas
Se, ao trabalhar com promessas, você encontrar o
Uncaught TypeError: undefined is not a promise
erro de
Uncaught TypeError: undefined is not a promise
, verifique se a
new Promise()
construção
new Promise()
é usada em vez de apenas
Promise()
ao criar promessas.
▍ design assíncrono / aguardado
A construção assíncrona / espera é uma abordagem moderna da programação assíncrona, simplificando-a. As funções assíncronas podem ser representadas como uma combinação de promessas e geradores e, em geral, essa construção é uma abstração das promessas.
O design assíncrono / espera reduz a quantidade de código padrão que você precisa escrever ao trabalhar com promessas. Quando as promessas apareceram no padrão ES2015, elas visavam solucionar o problema de criar código assíncrono. Eles lidaram com essa tarefa, mas em dois anos, compartilhando a saída dos padrões ES2015 e ES2017, ficou claro que eles não podiam ser considerados a solução final para o problema.
Um dos problemas prometidos foi o famoso "inferno de retornos de chamada", mas eles, resolvendo esse problema, criaram seus próprios problemas de natureza semelhante.
Promessas eram construções simples em torno das quais se poderia construir algo com uma sintaxe mais simples. Como resultado, quando chegou a hora, a construção assíncrona / aguardada apareceu. Seu uso permite que você escreva um código que pareça síncrono, mas é assíncrono, em particular, não bloqueia o segmento principal.
Como a construção assíncrona / espera funciona
Uma função assíncrona retorna uma promessa, como no exemplo a seguir:
const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) }) }
Quando você precisar chamar uma função semelhante, deverá colocar a palavra-chave
await
antes do comando para chamá-la. Isso fará com que o código que está aguardando a permissão ou a rejeição da promessa correspondente. Deve-se observar que uma função que usa a palavra-chave
await
deve ser declarada usando a
async
:
const doSomething = async () => { console.log(await doSomethingAsync()) }
Combine os dois fragmentos de código acima e examine seu comportamento:
const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) }) } const doSomething = async () => { console.log(await doSomethingAsync()) } console.log('Before') doSomething() console.log('After')
Este código produzirá o seguinte:
Before After I did something
O texto
I did something
entra no console com um atraso de 3 segundos.
Sobre promessas e funções assíncronas
Se você declarar uma determinada função usando a
async
, isso significa que essa função retornará uma promessa, mesmo que não seja explicitamente feita. É por isso que, por exemplo, o exemplo a seguir é um código de trabalho:
const aFunction = async () => { return 'test' } aFunction().then(console.log) // 'test'
Esse design é semelhante a este:
const aFunction = async () => { return Promise.resolve('test') } aFunction().then(console.log)
Pontos fortes de assíncrono / aguardar
Analisando os exemplos acima, você pode ver que o código que usa assíncrono / espera é mais simples que o código que usa o encadeamento de promessas ou código baseado em funções de retorno de chamada. Aqui, é claro, vimos exemplos muito simples. Você pode experimentar completamente os benefícios acima, trabalhando com código muito mais complexo. Aqui, por exemplo, é como carregar e analisar dados JSON usando promessas:
const getFirstUserData = () => { return fetch('/users.json') // .then(response => response.json()) // JSON .then(users => users[0]) // .then(user => fetch(`/users/${user.name}`)) // .then(userResponse => userResponse.json()) // JSON } getFirstUserData()
Aqui está a aparência da solução para o mesmo problema usando async / waitit:
const getFirstUserData = async () => { const response = await fetch('/users.json') // const users = await response.json() // JSON const user = users[0] // const userResponse = await fetch(`/users/${user.name}`) // const userData = await userResponse.json() // JSON return userData } getFirstUserData()
Usando sequências de funções assíncronas
Funções assíncronas podem ser facilmente combinadas em projetos que se assemelham a cadeias Promise. Os resultados dessa combinação, no entanto, são de melhor legibilidade:
const promiseToDoSomething = () => { return new Promise(resolve => { setTimeout(() => resolve('I did something'), 10000) }) } const watchOverSomeoneDoingSomething = async () => { const something = await promiseToDoSomething() return something + ' and I watched' } const watchOverSomeoneWatchingSomeoneDoingSomething = async () => { const something = await watchOverSomeoneDoingSomething() return something + ' and I watched as well' } watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => { console.log(res) })
Este código produzirá o seguinte texto:
I did something and I watched and I watched as well
Depuração simplificada
É difícil depurar as promessas, porque, ao usá-las, você não pode usar efetivamente as ferramentas usuais do depurador (como "ignorar etapa", ignorar). O código gravado usando async / waitit pode ser depurado usando os mesmos métodos que o código síncrono comum.
Geração de Eventos no Node.js
Se você trabalhou com JavaScript em um navegador, sabe que os eventos desempenham um papel importante no tratamento das interações do usuário com as páginas. Trata-se de manipular eventos causados por cliques e movimentos do mouse, pressionamentos de teclas no teclado e assim por diante. No Node.js, você pode trabalhar com eventos que o programador cria sozinho. Aqui você pode criar seu próprio sistema de eventos usando o módulo de
eventos . Em particular, este módulo nos oferece a classe
EventEmitter
,
EventEmitter
recursos podem ser usados para organizar o trabalho com eventos. Antes de usar esse mecanismo, você precisa conectá-lo:
const EventEmitter = require('events').EventEmitter
Ao trabalhar com ele, os métodos
on()
e
emit()
estão disponíveis para nós, entre outros. O método de
emit
usado para chamar eventos. O método
on
é usado para configurar retornos de chamada, manipuladores de eventos que são chamados quando um determinado evento é chamado.
Por exemplo, vamos criar um evento
start
. Quando isso acontecer, produziremos algo no console:
eventEmitter = new EventEmitter(); eventEmitter.on('start', () => { console.log('started') })
Para acionar esse evento, a seguinte construção é usada:
eventEmitter.emit('start')
Como resultado da execução desse comando, o manipulador de eventos é chamado e a sequência
started
chega ao console.
Você pode passar argumentos para o manipulador de eventos, representando-os como argumentos adicionais para o método
emit()
:
eventEmitter.on('start', (number) => { console.log(`started ${number}`) }) eventEmitter.emit('start', 23)
O mesmo acontece nos casos em que o manipulador precisa passar vários argumentos:
eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`) }) eventEmitter.emit('start', 1, 100)
EventEmitter
classe
EventEmitter
têm alguns outros métodos úteis:
once()
- permite registrar um manipulador de eventos que pode ser chamado apenas uma vez.removeListener()
- permite remover o manipulador passado a ele da matriz de manipuladores do evento passado a ele.removeAllListeners()
- permite remover todos os manipuladores do evento passado para ele.
Sumário
Hoje falamos sobre programação assíncrona em JavaScript, em particular, discutimos retornos de chamada, promessas e a construção assíncrona / aguardada. Aqui abordamos a questão de trabalhar com eventos descritos pelo desenvolvedor usando o módulo de
events
. Nosso próximo tópico serão os mecanismos de rede da plataforma Node.js.
Caros leitores! Ao programar para o Node.js, você usa a construção async / waitit?