Callbacks, promesas y funciones asíncronas en JavaScript: Parte 1
Spanish (Español) translation by Ruben Solis (you can also view the original English article)



Introducción
Se habla mucho sobre programación asíncrona, pero ¿cuál es exactamente el problema? El problema es que queremos que nuestro código sea no bloqueante.
Las tareas que pueden bloquear nuestra aplicación incluyen hacer peticiones HTTP, consultar una base de datos o abrir un archivo. Algunos lenguajes, como Java, se ocupan de esto creando múltiples hilos. Sin embargo, JavaScript tiene solo un hilo, por lo que debemos diseñar nuestros programas para que ninguna tarea bloquee el flujo.
La programación asíncrona resuelve este problema. Nos permite ejecutar tareas más tarde para no detener todo el programa esperando que se completen las tareas. También ayuda cuando queremos asegurarnos de que las tareas se ejecuten secuencialmente.
En la primera parte de este tutorial, aprenderemos los conceptos detrás del código síncrono y asíncrono y veremos cómo podemos usar las funciones callback para resolver problemas con la asincronía.
Contenido
- Hilos
- Síncrono vs. asíncrono
- Funciones callback
- Resumen
- Recursos
Hilos
Quiero que recuerdes la última vez que fuiste de compras al supermercado. Probablemente habían varias cajas registradoras abiertas para que los clientes puedan pagar. Esto ayuda a la tienda a procesar más transacciones en la misma cantidad de tiempo. Este es un ejemplo de concurrencia.
En pocas palabras, la concurrencia está haciendo más de una tarea al mismo tiempo. Tu sistema operativo es concurrente porque ejecuta múltiples procesos simultáneamente. Un proceso es un entorno de ejecución o una instancia de una aplicación en ejecución. Por ejemplo, tu navegador, editor de texto y software antivirus son procesos en tu computadora que se ejecutan simultáneamente.
Las aplicaciones también pueden ser concurrentes. Esto se logra con hilos. No profundizaré demasiado porque esto va más allá del alcance de este artículo. Si deseas una explicación detallada de cómo funciona JavaScript tras bambalinas, te recomiendo ver este video.
Un hilo es una unidad dentro de un proceso que está ejecutando código. En nuestro ejemplo de tienda, cada línea de pago sería un hilo. Si solo tenemos una línea de pago en la tienda, eso cambiaría la forma en que procesamos a los clientes.
¿Alguna vez has estado en línea y has tenido algo que detuvo tu transacción? Tal vez necesitabas una verificación de precios o tenías que ver a un gerente. Cuando estoy en la oficina de correos tratando de enviar un paquete y no tengo mis etiquetas llenas, el cajero me pide que me haga a un lado mientras continúan revisando a otros clientes. Cuando estoy listo, regreso al frente de la línea para que me revisen.
Esto es similar a cómo funciona la programación asíncrona. El cajero podría haberme esperado. A veces lo hacen. Pero es una mejor experiencia no mantener la cola y atender a los otros clientes. El punto es que los clientes no tienen que ser atendidos en el orden en que están en línea. Del mismo modo, el código no tiene que ejecutarse en el orden en que lo escribimos.
Síncrono vs. asíncrono
Es natural pensar en nuestro código ejecutándose secuencialmente de arriba a abajo. Esto es síncrono. Sin embargo, con JavaScript, algunas tareas son inherentemente asíncronas (por ejemplo, setTimeout), y algunas tareas las diseñamos para ser asíncronas porque sabemos de antemano que pueden ser bloqueantes.
Veamos un ejemplo práctico usando archivos en Node.js. Si deseas probar los ejemplos de código y necesitas una introducción al uso de Node.js, puedes encontrar las instrucciones de inicio en este tutorial. En este ejemplo, abriremos un archivo de publicaciones y recuperaremos una de ellas. Luego abriremos un archivo de comentarios y recuperaremos los comentarios para esa publicación.
Esta es la forma síncrona:
index.js
1 |
const fs = require('fs'); |
2 |
const path = require('path'); |
3 |
const postsUrl = path.join(__dirname, 'db/posts.json'); |
4 |
const commentsUrl = path.join(__dirname, 'db/comments.json'); |
5 |
|
6 |
//return the data from our file
|
7 |
function loadCollection(url) { |
8 |
try { |
9 |
const response = fs.readFileSync(url, 'utf8'); |
10 |
return JSON.parse(response); |
11 |
} catch (error) { |
12 |
console.log(error); |
13 |
}
|
14 |
}
|
15 |
|
16 |
//return an object by id
|
17 |
function getRecord(collection, id) { |
18 |
return collection.find(function(element){ |
19 |
return element.id == id; |
20 |
});
|
21 |
}
|
22 |
|
23 |
//return an array of comments for a post
|
24 |
function getCommentsByPost(comments, postId) { |
25 |
return comments.filter(function(comment){ |
26 |
return comment.postId == postId; |
27 |
});
|
28 |
}
|
29 |
|
30 |
//initialization code
|
31 |
const posts = loadCollection(postsUrl); |
32 |
const post = getRecord(posts, "001"); |
33 |
const comments = loadCollection(commentsUrl); |
34 |
const postComments = getCommentsByPost(comments, post.id); |
35 |
|
36 |
console.log(post); |
37 |
console.log(postComments); |
db/posts.json
1 |
[
|
2 |
{
|
3 |
"id": "001", |
4 |
"title": "Greeting", |
5 |
"text": "Hello World", |
6 |
"author": "Jane Doe" |
7 |
},
|
8 |
{
|
9 |
"id": "002", |
10 |
"title": "JavaScript 101", |
11 |
"text": "The fundamentals of programming.", |
12 |
"author": "Alberta Williams" |
13 |
},
|
14 |
{
|
15 |
"id": "003", |
16 |
"title": "Async Programming", |
17 |
"text": "Callbacks, Promises and Async/Await.", |
18 |
"author": "Alberta Williams" |
19 |
}
|
20 |
]
|
db/comments.json
1 |
[
|
2 |
{
|
3 |
"id": "phx732", |
4 |
"postId": "003", |
5 |
"text": "I don't get this callback stuff." |
6 |
},
|
7 |
{
|
8 |
"id": "avj9438", |
9 |
"postId": "003", |
10 |
"text": "This is really useful info." |
11 |
},
|
12 |
{
|
13 |
"id": "gnk368", |
14 |
"postId": "001", |
15 |
"text": "This is a test comment." |
16 |
}
|
17 |
]
|
El método readFileSync abre el archivo síncronamente. Por lo tanto, también podemos escribir nuestro código de inicialización de forma síncrona. Pero esta no es la mejor manera de abrir el archivo porque es una tarea potencialmente bloqueante. La apertura del archivo debe hacerse de forma asíncrona para que el flujo de ejecución pueda ser continuo.
Node tiene un método readFile que podemos usar para abrir el archivo de forma asíncrona. Esta es la sintaxis:
1 |
fs.readFile(url, 'utf8', function(error, data) { |
2 |
...
|
3 |
});
|
Puede que tengamos la tentación de devolver nuestros datos dentro de esta función callback, pero no estará disponible para que la usemos dentro de nuestra función loadCollection. Nuestro código de inicialización también tendrá que cambiar porque no tendremos los valores correctos para asignar a nuestras variables.
Para ilustrar el problema, veamos un ejemplo más simple. ¿Qué crees que imprimirá el siguiente código?
1 |
function task1() { |
2 |
setTimeout(function() { |
3 |
console.log('first'); |
4 |
}, 0); |
5 |
}
|
6 |
|
7 |
function task2() { |
8 |
console.log('second'); |
9 |
}
|
10 |
|
11 |
function task3() { |
12 |
console.log('third'); |
13 |
}
|
14 |
|
15 |
task1(); |
16 |
task2(); |
17 |
task3(); |
Este ejemplo imprimirá "second" (segundo, es español), "third" (tercero, en español) y luego "first" (primero, en español). No importa que la función setTimeout tenga un retraso de 0. Es una tarea asíncrona en JavaScript, por lo que siempre se aplazará para ejecutarse más tarde. La función firstTask puede representar cualquier tarea asíncrona, como abrir un archivo o consultar nuestra base de datos.
Una solución para que nuestras tareas se ejecuten en el orden que queremos es usar funciones callback.
Funciones callback
Para usar las funciones callback, pasa una función como parámetro a otra función y luego llama a la función cuando finaliza la tarea. Si necesitas un manual sobre cómo usar funciones de orden superior, reactivex tiene un tutorial interactivo que puedes probar.
Los callbacks nos permiten obligar a las tareas a ejecutarse secuencialmente. También nos ayudan cuando tenemos tareas que dependen de los resultados de una tarea anterior. Usando callbacks, podemos arreglar nuestro último ejemplo para que imprima "first", "second" y luego "third".
1 |
function first(cb) { |
2 |
setTimeout(function() { |
3 |
return cb('first'); |
4 |
}, 0); |
5 |
}
|
6 |
|
7 |
function second(cb) { |
8 |
return cb('second'); |
9 |
}
|
10 |
|
11 |
function third(cb) { |
12 |
return cb('third'); |
13 |
}
|
14 |
|
15 |
first(function(result1) { |
16 |
console.log(result1); |
17 |
second(function(result2) { |
18 |
console.log(result2); |
19 |
third(function(result3) { |
20 |
console.log(result3); |
21 |
});
|
22 |
});
|
23 |
});
|
En lugar de imprimir la cadena dentro de cada función, devolvemos el valor dentro de un callback. Cuando se ejecuta nuestro código, imprimimos el valor que se pasó a nuestro callback. Esto es lo que utiliza nuestra función readFile.
Volviendo al ejemplo de nuestros archivos, podemos cambiar nuestra función loadCollection para que use callbacks para leer el archivo de forma asíncrona.
1 |
function loadCollection(url, callback) { |
2 |
fs.readFile(url, 'utf8', function(error, data) { |
3 |
if (error) { |
4 |
console.log(error); |
5 |
} else { |
6 |
return callback(JSON.parse(data)); |
7 |
}
|
8 |
});
|
9 |
}
|
Y así es como se verá nuestro código de inicialización usando callbacks:
1 |
loadCollection(postsUrl, function(posts){ |
2 |
loadCollection(commentsUrl, function(comments){ |
3 |
getRecord(posts, "001", function(post){ |
4 |
const postComments = getCommentsByPost(comments, post.id); |
5 |
console.log(post); |
6 |
console.log(postComments); |
7 |
});
|
8 |
});
|
9 |
});
|
Una cosa a tener en cuenta en nuestra función loadCollection es que en lugar de usar una declaración try/catch para manejar errores, usamos una declaración if/else. El bloque catch no podría detectar errores devueltos por el callback readFile.
Es una buena práctica tener manejadores de errores en nuestro código para errores que son el resultado de influencias externas en lugar de errores de programación. Esto incluye acceder a archivos, conectarse a una base de datos o realizar una petición HTTP.
En el ejemplo de código revisado, no incluí ningún manejo de errores. Si ocurriera un error en cualquiera de los pasos, el programa no continuaría. Sería bueno proporcionar instrucciones significativas.
Un ejemplo de cuándo el manejo de errores es importante es si tenemos una tarea para iniciar sesión a un usuario. Esto implica obtener un nombre de usuario y una contraseña de un formulario, consultar nuestra base de datos para ver si es una combinación válida y luego redirigir al usuario a su tablero si tenemos éxito. Si el nombre de usuario y la contraseña no son válidos, nuestra aplicación dejaría de funcionar si no le decimos qué hacer.
Una mejor experiencia para el usuario sería devolver un mensaje de error al usuario y permitirle volver a intentar iniciar sesión. En nuestro ejemplo de archivo, podríamos pasar un objeto de error en la función callback. Este es el caso con la función readFile. Luego, cuando ejecutamos el código, podemos agregar una instrucción if/else para manejar el resultado exitoso y el resultado rechazado.
Tarea
Usando el método callback, escribe un programa que abra un archivo de usuarios, seleccione un usuario y luego abra un archivo de publicaciones e imprima la información del usuario y todas sus publicaciones.
Resumen
La programación asíncrona es un método utilizado en nuestro código para diferir eventos para su posterior ejecución. Cuando se trata de una tarea asíncrona, los callbacks son una solución para cronometrar nuestras tareas para que se ejecuten secuencialmente.
Si tenemos múltiples tareas que dependen del resultado de tareas anteriores, una solución es usar múltiples callbacks anidados. Sin embargo, esto podría conducir a un problema conocido como "callback hell". Las promesas resuelven el problema del callback hell y las funciones asíncronas nos permiten escribir nuestro código de forma síncrona. En la parte 2 de este tutorial, aprenderemos qué son y cómo usarlas en nuestro código.



