Hoy, en la traducción de la séptima parte del manual de Node.js, hablaremos sobre la programación asincrónica, consideraremos cuestiones como el uso de devoluciones de llamada, promesas y la construcción asíncrona / espera, y discutiremos cómo trabajar con eventos.

[Le aconsejamos que lea] Otras partes del cicloParte 1:
Información general y primeros pasosParte 2:
JavaScript, V8, algunos trucos de desarrolloParte 3:
Hosting, REPL, trabajar con la consola, módulosParte 4:
archivos npm, package.json y package-lock.jsonParte 5:
npm y npxParte 6:
bucle de eventos, pila de llamadas, temporizadoresParte 7:
Programación asincrónicaParte 8:
Guía de Node.js, Parte 8: Protocolos HTTP y WebSocketParte 9:
Guía de Node.js, parte 9: trabajar con el sistema de archivosParte 10:
Guía de Node.js, Parte 10: Módulos estándar, flujos, bases de datos, NODE_ENVPDF completo de la guía Node.js Asincronía en lenguajes de programación.
JavaScript en sí es un lenguaje de programación síncrono de un solo subproceso. Esto significa que no puede crear nuevos subprocesos en el código que se ejecutan en paralelo. Sin embargo, las computadoras son inherentemente asíncronas. Es decir, ciertas acciones se pueden realizar independientemente del flujo principal de ejecución del programa. En las computadoras modernas, a cada programa se le asigna una cierta cantidad de tiempo de procesador, cuando este tiempo se agota, el sistema le da recursos a otro programa, también por un tiempo. Tales cambios se realizan cíclicamente, se hace tan rápido que una persona simplemente no puede notarlo, como resultado, creemos que nuestras computadoras ejecutan muchos programas simultáneamente. Pero esto es una ilusión (sin mencionar las máquinas multiprocesador).
En las entrañas de los programas se utilizan interrupciones: señales transmitidas al procesador y que permiten atraer la atención del sistema. No entraremos en detalles, lo más importante es recordar que el comportamiento asíncrono, cuando el programa está en pausa hasta el momento en que necesita recursos del procesador, es completamente normal. En un momento en que el programa no carga el sistema con trabajo, la computadora puede resolver otros problemas. Por ejemplo, con este enfoque, cuando un programa espera una respuesta a una solicitud de red que se le hace, no bloquea el procesador hasta que se recibe una respuesta.
Como regla general, los lenguajes de programación son asíncronos, algunos de ellos le dan al programador la capacidad de controlar mecanismos asíncronos, utilizando las herramientas de lenguaje incorporadas o las bibliotecas especializadas. Estamos hablando de lenguajes como C, Java, C #, PHP, Go, Ruby, Swift, Python. Algunos de ellos le permiten programar en estilo asincrónico, utilizando hilos, comenzando nuevos procesos.
Asincronía JavaScript
Como ya se mencionó, JavaScript es un lenguaje síncrono de subproceso único. Las líneas de código escritas en JS se ejecutan en el orden en que aparecen en el texto, una tras otra. Por ejemplo, aquí hay un programa JS muy normal que demuestra este comportamiento:
const a = 1 const b = 2 const c = a * b console.log(c) doSomething()
Pero JavaScript fue creado para su uso en navegadores. Su tarea principal, al principio, era organizar el procesamiento de eventos relacionados con las actividades del usuario. Por ejemplo, estos son eventos como
onClick
,
onMouseOver
,
onChange
,
onSubmit
, etc. ¿Cómo resolver estos problemas en el marco de un modelo de programación síncrono?
La respuesta se encuentra en el entorno en el que se ejecuta JavaScript. A saber, el navegador le permite resolver eficazmente tales problemas, dándole al programador las API apropiadas.
En el entorno de Node.js existen herramientas para realizar operaciones de E / S sin bloqueo, como trabajar con archivos, organizar el intercambio de datos a través de una red, etc.
Devoluciones de llamada
Si hablamos de JavaScript basado en navegador, se puede observar que es imposible saber de antemano cuando el usuario hace clic en un botón. Para garantizar que el sistema responda a tal evento, se crea un controlador para ello.
El controlador de eventos acepta una función que se llamará cuando ocurra el evento. Se ve así:
document.getElementById('button').addEventListener('click', () => { // })
Dichas funciones también se denominan funciones de devolución de llamada o devoluciones de llamada.
Una devolución de llamada es una función regular que se pasa como valor a otra función. Solo se llamará cuando ocurra un evento determinado. JavaScript implementa el concepto de funciones de primera clase. Dichas funciones pueden asignarse a variables y pasarse a otras funciones (llamadas funciones de orden superior).
En el desarrollo de JavaScript del lado del cliente, el enfoque se generaliza cuando todo el código del cliente se envuelve en un escucha del evento de
load
de un objeto de
window
, que llama a la devolución de llamada que se le pasa después de que la página está lista para trabajar:
window.addEventListener('load', () => { // // })
Las devoluciones de llamada se usan en todas partes, y no solo para manejar eventos DOM. Por ejemplo, ya nos hemos encontrado con su uso en temporizadores:
setTimeout(() => {
Las solicitudes XHR también usan devoluciones de llamada. En este caso, parece asignar una función a la propiedad correspondiente. Se llamará a una función similar cuando ocurra un evento determinado. En el siguiente ejemplo, dicho evento es un cambio de estado de solicitud:
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()
▍ Manejo de errores en devoluciones de llamada
Hablemos sobre cómo manejar errores en devoluciones de llamada. Hay una estrategia común para manejar tales errores, que también se usa en Node.js. Consiste en el hecho de que el primer parámetro de cualquier función de devolución de llamada es un objeto de error. Si no hay errores, se escribirá
null
en este parámetro. De lo contrario, habrá un objeto de error que contiene su descripción e información adicional al respecto. Así es como se ve:
fs.readFile('/file.json', (err, data) => { if (err !== null) { // console.log(err) return } // , console.log(data) })
▍ Problema de devolución de llamada
Las devoluciones de llamada son convenientes para usar en situaciones simples. Sin embargo, cada devolución de llamada es un nivel adicional de anidación de código. Si se utilizan varias devoluciones de llamada anidadas, esto rápidamente conduce a una complicación significativa de la estructura del código:
window.addEventListener('load', () => { document.getElementById('button').addEventListener('click', () => { setTimeout(() => { items.forEach(item => { //, - }) }, 2000) }) })
En este ejemplo, solo se muestran 4 niveles de código, pero en la práctica uno puede encontrar una gran cantidad de niveles, generalmente llamados el "infierno de devolución de llamada". Puede resolver este problema utilizando otras construcciones de lenguaje.
Promesas y asíncrono / espera
Comenzando con el estándar ES6, JavaScript introduce nuevas características que facilitan la escritura de código asincrónico, eliminando la necesidad de devoluciones de llamada. Estamos hablando de las promesas que aparecieron en ES6, y la construcción asíncrona / en espera que apareció en ES8.
▍ Promesas
Las promesas (objetos de promesa) son una de las formas de trabajar con construcciones de software asincrónicas en JavaScript, lo que, en general, reduce el uso de devoluciones de llamada.
Conocimiento de promesas
Las promesas generalmente se definen como objetos proxy para ciertos valores, cuya apariencia se espera en el futuro. Las promesas también se llaman "promesas" o "resultados prometidos". Aunque este concepto existe desde hace muchos años, las promesas se estandarizaron y se agregaron al lenguaje solo en ES2015. En ES2017, apareció el diseño asíncrono / espera, que se basa en promesas y que puede considerarse como su reemplazo conveniente. Por lo tanto, incluso si no está planeando usar promesas regulares, es importante comprender cómo funcionan para utilizar el constructo async / wait de manera efectiva.
Cómo funcionan las promesas
Después de que se llama una promesa, pasa a un estado pendiente. Esto significa que la función que causó la promesa continúa ejecutándose, mientras que se realizan algunos cálculos en la promesa, después de lo cual la promesa informa al respecto. Si la operación realizada por la promesa se completa con éxito, la promesa se transfiere al estado cumplido. Dicha promesa se dice que se resuelve con éxito. Si la operación se completa con un error, la promesa se coloca en el estado rechazado.
Hablemos de trabajar con promesas.
Crea promesas
La API para trabajar con promesas nos proporciona el constructor correspondiente, que se llama mediante un comando de la forma
new Promise()
. Así es como se crean las promesas:
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) } } )
Promis comprueba la constante global
done
, y si su valor es
true
, se resuelve con éxito. De lo contrario, la promesa es rechazada. Usando los parámetros
resolve
y
reject
, que son funciones, podemos devolver valores de la promesa. En este caso, devolvemos una cadena, pero aquí se puede usar un objeto.
Trabaja con promesas
Creamos una promesa arriba, ahora considera trabajar con ella. Se ve así:
const isItDoneYet = new Promise( //... ) const checkIfItsDone = () => { isItDoneYet .then((ok) => { console.log(ok) }) .catch((err) => { console.error(err) }) } checkIfItsDone()
Llamar a
checkIfItsDone()
conducirá a la ejecución de la
isItDoneYet()
isItDoneYet
isItDoneYet()
y a la organización de esperar su resolución. Si la promesa se resuelve con éxito, la devolución de llamada pasada al método
.then()
funcionará. Si se produce un error, es decir, la promesa será rechazada, se puede procesar en la función que se pasa al método
.catch()
.
Promesas encadenadas
Los métodos de promesa devuelven promesas, lo que le permite combinarlos en cadenas. Un buen ejemplo de este comportamiento es el
API Fetch basado en navegador, que es una capa de abstracción sobre
XMLHttpRequest
. Hay un paquete npm bastante popular para Node.js que implementa la API Fetch, que discutiremos más adelante. Esta API se puede utilizar para cargar ciertos recursos de red y, gracias a la posibilidad de combinar promesas en cadenas, organizar el procesamiento posterior de los datos descargados. De hecho, cuando llama a la API Fetch a través de una llamada a la función
fetch()
, se crea una promesa.
Considere el siguiente ejemplo de encadenamiento de promesas:
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) })
Aquí usamos el paquete npm
node-fetch y el recurso
jsonplaceholder.typicode.com como fuente de datos JSON.
En este ejemplo, la función
fetch()
se usa para cargar un elemento de lista TODO usando una cadena de promesas. Después de ejecutar
fetch()
,
se devuelve una
respuesta que tiene muchas propiedades, entre las cuales estamos interesados en lo siguiente:
status
es un valor numérico que representa el código de estado HTTP.statusText
: una descripción textual del código de estado HTTP, que se representa con la cadena OK
si la solicitud se realizó correctamente.
El objeto de
response
tiene un método
json()
que devuelve una promesa, tras la resolución de la cual se presenta el contenido procesado del cuerpo de la solicitud, presentado en formato JSON.
Dado lo anterior, describimos lo que está sucediendo en este código. La primera promesa en la cadena está representada por la función
status()
que anunciamos, que verifica el estado de la respuesta, y si indica que la solicitud falló (es decir, el código de estado HTTP no está en el rango entre 200 y 299), la promesa se rechaza. Esta operación lleva al hecho de que otras expresiones
.then()
en la cadena de promesas no se ejecutan e inmediatamente llegamos al método
.catch()
, que se envía a la consola, junto con el mensaje de error, el texto
Request failed
.
Si el código de estado HTTP nos conviene, se llama a la función
json()
declarada por nosotros. Dado que la promesa anterior, si se resuelve con éxito, devuelve un objeto de
response
, lo usamos como valor de entrada para la segunda promesa.
En este caso, devolvemos los datos JSON procesados, por lo que la tercera promesa los recibe, luego de lo cual, precedidos por un mensaje que indica que como resultado de la solicitud fue posible obtener los datos necesarios, se muestran en la consola.
Manejo de errores
En el ejemplo anterior, teníamos un método
.catch()
adjunto a una cadena de promesas. Si algo en la cadena de promesas sale mal y ocurre un error, o si una de las promesas resulta ser rechazada, el control se transfiere a la expresión más cercana
.catch()
. Aquí está la situación cuando ocurre un error en una promesa:
new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { console.error(err) })
Aquí hay un ejemplo de activación de
.catch()
después de rechazar una promesa:
new Promise((resolve, reject) => { reject('Error') }) .catch((err) => { console.error(err) })
Manejo de errores en cascada
¿Qué sucede si se produce un error en la expresión
.catch()
? Para manejar este error, puede incluir otra expresión
.catch()
en la cadena de promesas (y luego puede adjuntar tantas expresiones
.catch()
a la cadena como sea necesario):
new Promise((resolve, reject) => { throw new Error('Error') }) .catch((err) => { throw new Error('Error') }) .catch((err) => { console.error(err) })
Ahora echemos un vistazo a algunos métodos útiles utilizados para gestionar las promesas.
Promise.all ()
Si necesita realizar alguna acción después de resolver varias promesas, puede hacerlo utilizando el
Promise.all()
. Considere un ejemplo:
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) })
En ES2015, apareció la sintaxis de la asignación destructiva; al usarla, puede crear construcciones de la siguiente forma:
Promise.all([f1, f2]).then(([res1, res2]) => { console.log('Results', res1, res2) })
Aquí, como ejemplo, consideramos el API Fetch, pero
Promise.all()
, por supuesto, le permite trabajar con cualquier promesa.
Promise.race ()
El
Promise.race()
permite realizar la acción especificada después de que se resuelva una de las promesas que se le pasaron. La devolución de llamada correspondiente que contiene los resultados de esta primera promesa se llama solo una vez. Considere un ejemplo:
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)
Error TypeError no detectado que se produce al trabajar con promesas
Si, al trabajar con promesas, encuentra el Error de tipo
Uncaught TypeError: undefined is not a promise
error de
Uncaught TypeError: undefined is not a promise
, asegúrese de que se use la
new Promise()
construcción
new Promise()
lugar de solo
Promise()
al crear promesas.
▍ diseño asíncrono / espera
La construcción async / await es un enfoque moderno para la programación asincrónica, simplificándola. Las funciones asincrónicas se pueden representar como una combinación de promesas y generadores, y, en general, esta construcción es una abstracción sobre las promesas.
El diseño asíncrono / espera reduce la cantidad de código repetitivo que debe escribir cuando trabaja con promesas. Cuando aparecieron las promesas en el estándar ES2015, su objetivo era resolver el problema de la creación de código asincrónico. Hicieron frente a esta tarea, pero en dos años, compartiendo el resultado de los estándares ES2015 y ES2017, quedó claro que no podían considerarse la solución final del problema.
Uno de los problemas que las promesas resolvieron fue el famoso "infierno de devoluciones de llamada", pero ellos, resolviendo este problema, crearon sus propios problemas de naturaleza similar.
Las promesas eran construcciones simples alrededor de las cuales uno podía construir algo con una sintaxis más simple. Como resultado, cuando llegó el momento, apareció la construcción asíncrona / espera. Su uso le permite escribir código que parece síncrono, pero es asíncrono, en particular, no bloquea el hilo principal.
Cómo funciona la construcción async / await
Una función asincrónica devuelve una promesa, como en el siguiente ejemplo:
const doSomethingAsync = () => { return new Promise((resolve) => { setTimeout(() => resolve('I did something'), 3000) }) }
Cuando necesite llamar a una función similar, debe colocar la palabra clave
await
antes del comando para llamarla. Esto hará que el código que lo llama espere el permiso o el rechazo de la promesa correspondiente. Cabe señalar que una función que usa la palabra clave
await
debe declararse usando la
async
:
const doSomething = async () => { console.log(await doSomethingAsync()) }
Combine los dos fragmentos de código anteriores y examine su comportamiento:
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 generará lo siguiente:
Before After I did something
El texto
I did something
entra en la consola con un retraso de 3 segundos.
Sobre promesas y funciones asincrónicas
Si declara una determinada función utilizando la
async
, esto significará que dicha función devolverá una promesa incluso si no se hace explícitamente. Por eso, por ejemplo, el siguiente ejemplo es un código de trabajo:
const aFunction = async () => { return 'test' } aFunction().then(console.log) // 'test'
Este diseño es similar a este:
const aFunction = async () => { return Promise.resolve('test') } aFunction().then(console.log)
Fortalezas de asíncrono / espera
Al analizar los ejemplos anteriores, puede ver que el código que usa async / await es más simple que el código que usa el encadenamiento de promesas o el código basado en funciones de devolución de llamada. Aquí, por supuesto, miramos ejemplos muy simples. Puede experimentar plenamente los beneficios anteriores trabajando con un código mucho más complejo. Aquí, por ejemplo, se explica cómo cargar y analizar datos JSON usando promesas:
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()
Así es como se ve la solución al mismo problema usando async / await:
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 secuencias de funciones asincrónicas
Las funciones asincrónicas se pueden combinar fácilmente en diseños que se parecen a las cadenas de Promise. Los resultados de tal combinación, sin embargo, son una legibilidad mucho mejor:
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 generará el siguiente texto:
I did something and I watched and I watched as well
Depuración simplificada
Las promesas son difíciles de depurar, porque al usarlas no se pueden usar de manera efectiva las herramientas habituales del depurador (como "omisión de pasos", paso a paso). El código escrito usando async / await se puede depurar usando los mismos métodos que el código síncrono normal.
Generación de eventos en Node.js
Si trabajó con JavaScript en un navegador, entonces sabe que los eventos juegan un papel muy importante en el manejo de las interacciones de los usuarios con las páginas. Se trata de manejar eventos causados por clics y movimientos del mouse, pulsaciones de teclas en el teclado, etc. En Node.js, puede trabajar con eventos que el programador crea por sí mismo. Aquí puede crear su propio sistema de eventos utilizando el módulo de
eventos . En particular, este módulo nos ofrece la clase
EventEmitter
,
EventEmitter
capacidades se pueden utilizar para organizar el trabajo con eventos. Antes de usar este mecanismo, debe conectarlo:
const EventEmitter = require('events').EventEmitter
Al trabajar con él, los métodos
on()
y
emit()
están disponibles para nosotros, entre otros. El método de
emit
usa para llamar a eventos. El método
on
se utiliza para configurar devoluciones de llamada, controladores de eventos que se llaman cuando se llama a un determinado evento.
Por ejemplo, creemos un evento de
start
. Cuando sucede, enviaremos algo a la consola:
eventEmitter = new EventEmitter(); eventEmitter.on('start', () => { console.log('started') })
Para desencadenar este evento, se utiliza la siguiente construcción:
eventEmitter.emit('start')
Como resultado de la ejecución de este comando, se llama al controlador de eventos y la cadena
started
llega a la consola.
Puede pasar argumentos al controlador de eventos, representándolos como argumentos adicionales para el método
emit()
:
eventEmitter.on('start', (number) => { console.log(`started ${number}`) }) eventEmitter.emit('start', 23)
Lo mismo sucede en los casos en que el controlador necesita pasar varios argumentos:
eventEmitter.on('start', (start, end) => { console.log(`started from ${start} to ${end}`) }) eventEmitter.emit('start', 1, 100)
EventEmitter
clase
EventEmitter
tienen algunos otros métodos útiles:
once()
: le permite registrar un controlador de eventos al que solo se puede llamar una vez.removeListener()
: le permite eliminar el controlador que se le pasó de la matriz de controladores del evento que se le pasó.removeAllListeners()
: le permite eliminar todos los controladores del evento que se le pasó.
Resumen
Hoy hablamos sobre programación asincrónica en JavaScript, en particular, discutimos devoluciones de llamada, promesas y la construcción async / wait. Aquí tocamos el tema de trabajar con eventos descritos por el desarrollador usando el módulo de
events
. Nuestro próximo tema serán los mecanismos de red de la plataforma Node.js.
Estimados lectores! Al programar para Node.js, ¿utiliza la construcción async / await?