Sprint - 10 1
Sprint - 10 1
https://youtu.be/UKFG-S_cXWQ
Megan había estado demasiado ocupada haciendo malabares con sus estudios de
desarrollo web y cuidando de un niño pequeño como para visitar el nuevo centro
comunitario. Pero hoy es el día: Megan ha sido citada en el centro. Se va a reunir
con Carmela Mitsouko, una talentosa ingeniera que ha creado un prototipo de un
dispositivo innovador que traduce los balbuceos de los niños pequeños en
peticiones que los padres pueden entender. Si funciona, el dispositivo le ahorrará
a Megan un montón de tiempo que podrá utilizar para crear páginas web.
El arte de trabajar con APIs, que será lo que empezarás a dominar en este sprint,
en realidad se parece bastante a la invención de Carmela. Aprenderás a
comunicarte con un servidor enviando solicitudes HTTP desde el front-end, para
procesar los datos que llegan desde el back-end y a mostrar esos datos a los
usuarios.
¿De qué trata este sprint?
https://youtu.be/BfV6eqmOMI8
Operaciones asíncronas
Megan es la prueba de que no todas las heroínas llevan capa. Su capacidad para
hacer varias tareas a la vez en medio del caos es algo increíble. Compone música
rock mientras mece a la pequeña Payton, y si tiene la suerte de que su hija
pequeña se duerma durante unos minutos, aprovecha ese tiempo para escribir y
probar un par de funciones para sus proyectos de Practicum, intentando no
perturbar la breve siesta de su hija.
Y es verdad que Javascript no puede hacerse cargo de los niños (los nodos hijos
no cuentan). Sin embargo, JavaScript y Megan tienen algo en común; ambos
pueden realizar tareas de forma no lineal. Esto se conoce como asincronía.
¿Qué es el código síncrono?
En cuanto a las operaciones, pueden ser síncronas o asíncronas. Veamos cada una
de ellas con más detalle.
Pongamos de ejemplo una tarea que se da a menudo en las entrevistas de trabajo
y que requiere que el candidato escriba una función que devuelva los números de
la secuencia de Fibonacci:
Copiar códigoJAVASCRIPT
function computeFibonacciElement(n) {
if (n <= 2) return 1;
console.log(computeFibonacciElement(1000)); // 4.346655768693743e+208
Nos puede parecer que el motor JS calculó este resultado al instante, pero en
realidad tardó unos 30 milisegundos. El resultado solo se registra en la consola
una vez realizado el cálculo. Este retraso puede no parecer mucho, pero puede
tener un efecto significativo dependiendo de las circunstancias.
En nuestro caso, no importa porque no tiene sentido registrar el resultado en la
consola antes de que se haya calculado, de tal forma que el orden de los eventos
tiene sentido. Sin embargo, si nuestro código tuviera otras tareas que realizar,
éstas tendrían que estar en cola esperando a ser ejecutadas hasta que
transcurrieran los 30 milisegundos, lo que en algunos casos puede afectar a la
ejecución de nuestro programa.
Por defecto, las operaciones de JavaScript se ejecutan de forma lineal:
Como resultado, obtenemos una especie de cola de operaciones, en la que se
impide la ejecución de cada fragmento de código hasta que se haya ejecutado
todo lo que le precede. Este tipo de comportamiento se denomina código
síncrono.
Podemos comparar el código síncrono con darnos una ducha. En primer lugar,
tienes que meterte en la ducha y contemplar el sentido de la vida durante un
rato. Luego, te enjabonas. Y una vez hecho esto, aclaras el jabón. No puedes pasar
al siguiente paso hasta no haber completado el anterior.
Del mismo modo, el código síncrono ejecuta cada comando siguiendo una
secuencia. Solo avanza al siguiente paso una vez que el anterior esté completo.
¿Qué es el código asíncrono?
Hay casos en los que las cosas no se ponen en cola de forma estrictamente lineal
como se ha descrito anteriormente. Al terminar de ducharte, vas a prepararte el
desayuno. Imaginémonos que quieres prepararte una tortilla y una buena taza de
café. Así que pones la sartén en el fuego, añades un poco de aceite y rompes unos
huevos. Muy bien. Mientras se cocina la tortilla, puedes empezar a prepararte un
café.
No es necesario esperar a que la tortilla termine de cocinarse para hacer esto,
aunque hay que realizar los pasos de cada tarea individual siguiendo una
secuencia. No se puede sacar la sartén del fuego antes de que la tortilla esté lista;
y no se puede encender la cafetera antes de haber puesto un poco de café en el
filtro y haber llenado la máquina de agua.
Este tipo de procesos pueden compararse con los métodos que toman callbacks
como argumento. Por ejemplo, piensa en el método addEventListener(), que dice
que cuando ocurre un evento (la tortilla está lista), algún otro evento (¡saca la
sartén del fuego antes de que se quemen los huevos!) debe dispararse:
Copiar códigoJAVASCRIPT
// más código
El motor de JavaScript no es lo único que interactúa con una página web. Crear la
experiencia que recibirá el usuario también incluye el hardware del ordenador,
como una CPU, un teclado y una tarjeta de red. Como el motor no puede
comunicarse con estos dispositivos directamente, necesita una interfaz que sirva
de puente entre JavaScript y el hardware externo.
Este tipo de interfaz se conoce como API (abreviatura inglesa de Application
Programming Interface o interfaz de programación de aplicaciones), que es un
conjunto de comandos que rigen la forma en que el motor JS interactúa con el
hardware del PC. Esto nos permite especificar el comportamiento de nuestra
página web en función de la entrada de diferentes tipos de dispositivos.
Así es como el motor de JavaScript interactúa con los dispositivos a través de una
API:
Encuentra un trozo de código que requiere algún tipo de interferencia de
un dispositivo físico para ser ejecutado. Por ejemplo, podría ser un
controlador de eventos que detecte un clic del ratón.
A continuación, el motor se remite a la API como si dijera: "Estoy
esperando a que hagan clic sobre mi. Si esto sucede, me darás el objeto
event y me darás la señal de que debo ejecutar este código".
Cuando se produce el evento de clic, la API recoge todos los datos sobre el
objeto event y los entrega al motor JS.
Cuando el motor recibe el objeto event, ejecuta el código en el callback.
Una API puede contener instrucciones para cualquier tipo de evento, como pulsar
el botón de algún dispositivo Bluetooth, recibir una señal de una tarjeta de red o
algún movimiento del cursor. En cualquier caso, la lógica básica del
funcionamiento de la API es la misma. El motor JS se comunica con la API y espera
que esta responda con ciertos datos. Cuando se reciben estos datos, el motor
ejecuta el código correspondiente.
Por lo tanto, necesitamos código asíncrono para que nuestra página web sea
capaz de interactuar con el usuario, así como con el dispositivo que se utiliza para
acceder a la página, al mismo tiempo que permite que diferentes procesos tengan
lugar en paralelo. Hay diferentes maneras de trabajar con código asíncrono. En las
próximas unidades, hablaremos de cómo podemos hacerlo utilizando funciones
de callback.
Callbacks
const tweets = [
"algún hilo raro",
"una respuesta al tweet de Elon Musk",
"reacción a las noticias de última hora"
];
tweets.forEach(function (tweet) {
console.log(tweet);
});
Un callback puede ser una función anónima. También se puede declarar de forma
independiente y pasar por el nombre:
Copiar códigoJAVASCRIPT
const tweets = [
"algún hilo raro",
"una respuesta al tweet de Elon Musk",
"reacción a las noticias de última hora"
];
function consoleTweet(tweet) {
console.log(tweet);
}
tweets.forEach(consoleTweet);
¿Pero qué pasa si algo sale mal? Digamos que no podemos encontrar el
contenedor por su selector. Para tener en cuenta esta posibilidad, tenemos que
añadir una comprobación:
Copiar códigoJAVASCRIPT
function handleError(tweet) {
const newTweetContainer = document.createElement("div");
newTweetContainer.textContent = tweet;
document.body.append(newTweetContainer);
}
// añade un tercer parámetro, que es un callback
function insertTweet(tweet, containerSelector, callback) {
const tweetContainer = document.querySelector(containerSelector);
// si no se encuentra el contenedor adecuado, vamos a crearlo
if (!tweetContainer) {
callback(tweet);
return;
} tweetContainer.textContent = tweet;
insertTweet(
"una respuesta al tweet de Elon Musk",
".tweets",
function () {
const newTweetContainer = document.createElement("div");
newTweetContainer.textContent = tweet;
document.body.append(newTweetContainer);
}
);
Configuramos el callback para que funcione con código síncrono. Cada bloque de
código se ejecuta uno tras otro, y la secuencia ya está determinada. Esto es
completamente diferente cuando tu código tiene que esperar a que ocurra algo.
En este caso, nuestro código es asíncrono, y los callbacks son muy útiles en este
caso. Hablaremos de esto en la próxima unidad, después de practicarlo un poco.
Lección 2 Callbacks
Escribe tu propia implementación del método forEach() que tenga dos
parámetros: un array y un callback. La función debe hacer un bucle sobre el array
sin utilizar los métodos forEach(), map() o reduce().
const tweets = [
"algún hilo raro",
"una respuesta al tweet de Elon Musk",
"reacción a las noticias de última hora"
];
function forEach(arr, callback) {
// escribe tu código aquí
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i, arr);
}
}
forEach(tweets, function (tweet) {
console.log(tweet);
});
Callbacks asíncronos
En la unidad anterior, aprendimos sobre los callbacks síncronos. Ahora es el
momento de hablar de los callbacks asíncronos.
Callbacks utilizados para subir imágenes
Consideremos el siguiente ejemplo: necesitamos escribir una función para cargar
imágenes en un sitio web. Para ello, primero crearemos un elemento <img>
llamando al método document.createElement(). A continuación, asignaremos el
atributo src a la imagen para que el navegador sepa desde dónde cargarla:
Copiar códigoJAVASCRIPT
function loadImage(imageUrl) {
// Crea el objeto imagen (objeto de la imagen)
const img = document.createElement("img");
img.src = imageUrl; // Especifica la ruta de acceso a la imagen
return img;
}
Para evitar estas situaciones, necesitamos crear un nodo DOM después de cargar
la imagen. Es en estos casos donde los callbacks resultan útiles.
El objeto de la imagen tiene dos propiedades: onload y onerror, en las que
podemos colocar funciones. La primera función será invocada cuando se cargue la
imagen. La segunda se ejecutará cuando se produzca un error.
Estas funciones deben ser almacenadas en los métodos de los objetos
correspondientes:
Copiar códigoJAVASCRIPT
Bien! Ahora el elemento DOM se creará solo después de que se cargue la imagen,
por lo que el diseño no tendrá más fallos.
Este es un ejemplo de cómo trabajar con código asíncrono. Renderizar imágenes
en una página es parte del trabajo de la API del navegador, y la interacción con
esta API es asíncrona. Como puedes ver, tras pedir al servidor que cargue la
imagen, el navegador ha empezado a ejecutar otro trozo de código.
Nuestra tarea consistía en gestionar la interacción con el código asíncrono. En
primer lugar, le indicamos al motor que renderice la imagen solo cuando se
cargue. Para indicarle eso, colocamos la función para renderizar la imagen en la
propiedad onload.
Los callbacks son una forma de trabajar con el código. Sin embargo, este enfoque
tiene varios inconvenientes, que discutiremos en la próxima unidad.
function handleLoadError(){
console.log('Image not loaded. ERROR! ERROR!');
}
function handleImageLoad(evt) {
// Añade el elemento de la imagen al DOM después de cargar la imagen
document.body.append(evt.target);
}
// Completa el código de esta función
function loadImage(imageUrl, loadCallback, errorCallback) {
const img = document.createElement("img");
img.src = imageUrl;
img.onload = loadCallback;
img.onerror = errorCallback;
}
loadImage(
"https://pictures.s3.yandex.net/frontend-developer/functions/dog-12345.jpg",
handleImageLoad,
handleLoadError
);
Temporizadores
Copiar códigoJAVASCRIPT
function showMessage(message) {
console.log(message);
}
function logOut() {
// La lógica de cerrar la sesión del usuario después de un cierto período de inactividad
//No te preocupes. Pronto serás capaz de escribir esa lógica tú mismo
}
// Cierra la sesión del usuario en el sistema una vez transcurridos 300 segundos
let timer = setTimeout(logOut, 300000);
function checkEmail() {
// Aquí está el código para comprobar los nuevos correos electrónicos.
// Pronto podrás escribir ese código tú mismo.
}
// Si el usuario ha vuelto,
window.addEventListener("focus", function() {
// el evento focus se dispara cuando el usuario hace clic en el elemento o pulsa la tecla Tab
interval = setInterval(checkEmail, 10000); // inicia el temporizador de nuevo.
}
Sea cual sea el tiempo que establezcas para los métodos setTimeout() o
setInterval(), el retraso o intervalo real siempre será un poco mayor debido a la
cantidad de acciones que el procesador tiene que realizar. Cuando pasas un
callback, se añade a la cola de tareas.
Lección 4 Temporizadores
Hemos escrito una función para registrar la hora actual en la consola. Termina el
código y escríbelo para que la hora actual se registre en la consola cada segundo.
function consoleDate() {
const date = new Date();
console.log(`${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`);
}
Bucle de eventos
En las unidades anteriores, hemos hablado del comportamiento asíncrono y de las formas en que
podemos trabajar con él utilizando callbacks. A veces, puede ser difícil entender el orden exacto en el que
se ejecutará el código asíncrono. Veamos el siguiente ejemplo:
Copiar códigoJAVASCRIPT
setTimeout(function () {
console.log("Este mensaje aparecerá en tercer lugar");
}, 1);
En primer lugar, el motor ejecuta la función console.log() en la primera línea de código. Para ello, el
motor coloca console.log() en una cola junto a otras llamadas a funciones que están esperando su
ejecución. Esta cola recibe el nombre de pila de llamadas.
A continuación, el motor pasa a la función setTimeout(). Sin embargo, el motor no puede calcular
realmente el tiempo; esto lo hace el navegador. Así que, el motor se remite a la API del navegador y le
pide que establezca un temporizador de 1 ms.
Por lo tanto, la petición ha sido enviada a la API, y el motor procede a su siguiente tarea, que en este caso
es añadir la siguiente llamada a console.log() a la pila de llamadas; es decir, la que está en la línea inferior
de código.
Copiar códigoJAVASCRIPT
La tarea para registrar el string "Eeny, meeny, miny, moe" se introduce en la pila de llamadas.
El motor ejecuta la tarea y registra el string en la consola.
La siguiente tarea se introduce en la pila de llamadas: registra el string "Catch a tiger by the toe".
El motor ejecuta esta tarea y registra otro string en la consola.
Pero también se pueden emplear tareas mucho más complejas que produzcan otras tareas:
Copiar códigoJAVASCRIPT
function workHardPlayHard() {
function work() {
console.log("Trabaja duro");
}
function play() {
console.log("Juega duro");
}
work();
play();
}
workHardPlayHard();
Podemos pensar en la pila de llamadas como un conjunto de objetos físicos apilados unos sobre otros.
Imagina una baraja de cartas, por ejemplo. Antes de poder sacar la carta que está en el fondo del mazo,
primero hay que eliminar todas las que están encima.
Así es como funciona la pila de llamadas cuando tenemos funciones anidadas. Echa otro vistazo a la
imagen de arriba. La tarjeta workHardPlayHard() es la primera que se añade a la pila, seguida de la
tarjeta work() y, por último, la tarjeta console.log(). En este punto, la tarjeta console.log() está en la parte
superior de la pila. Si queremos eliminar cualquier otra carta de la pila, tenemos que empezar por ésta.
Por lo tanto, siempre que tengamos funciones anidadas, el motor trabajará hacia adentro cuando las
añada a la pila de llamadas, y luego trabajará hacia afuera cuando las borre.
Copiar códigoJAVASCRIPT
setTimeout(function () {
console.log("Este mensaje aparecerá en tercer lugar");
}, 1);
Copiar códigoJAVASCRIPT
setTimeout(function () {
console.log("¿Cuándo crees que se registrará este mensaje?");
}, 1);
Si no estás familiarizado con el bucle de eventos (Event Loop), puedes pensar en lo siguiente: como se
tarda más de 1ms en mostrar un string en la consola un millón de veces, el mensaje "¿Cuándo crees que
se registrará este mensaje?" se registrará en algún lugar entre la secuencia de registros "Este mensaje se
registrará un millón uno de veces".
Sin embargo, eso es incorrecto. Veamos cómo el motor JS procesará realmente estas llamadas. Enviará
las tareas a la pila de llamadas en el siguiente orden:
Después de eso, el motor recibe una señal de la API del navegador diciendo que el temporizador se ha
apagado. Ahora tiene que ejecutar el código de callback, es decir, registrar el string "¿Cuándo crees que se
registrará este mensaje?" en la consola. Entonces, el motor envía la tarea a la pila de llamadas.
El string "¿Cuándo crees que se registrará este mensaje?" se registra en la consola.
Así, cuando se produce un evento, la tarea se añade a la cola de tareas. A continuación, el bucle de
eventos recoge las tareas de la cola y las pasa a la pila de llamadas para ejecutarlas.
Pregunta
Copiar códigoJAVASCRIPT
console.log(1);
1, 2, 3
2, 1, 3
1, 3, 2
Comprobar respuesta
Promises
Hace un tiempo, cuando todo el mundo seguía utilizando jQuery, el código para
ejecutar callbacks en secuencias podía ser similar a lo que mostramos a
continuación (no esperamos que entiendas cada línea de esta monstruosidad):
Copiar códigoJAVASCRIPT
// una función que muestra cuántos amigos comunes tienen Tristan y Kristen
$.ajax({
url: "https://socialnetwork.com/users",
success: function (data) {
// encuentra el usuario necesario
const tristan = data.users.find(user => user.name === "Tristan");
if (isMutual) {
mutualFriends += 1;
}
}
ajax("https://socialnetwork.com/users")
.then(getTristan)
.then(getKristen)
.then(getMutualFriends)
.then((mutualFriends) => {
alert(`Kristen y Tristan tienen ${mutualFriends} amigos mutuos`);
});
Es una gran mejora, ¿no lo crees? El nuevo código es más fácil y mucho más
agradable de leer. Ahora, vamos a desglosar la forma en la que realmente
funciona.
El concepto de promises
En algunas cafeterías y restaurantes de comida rápida, después de hacer un
pedido, recibes un localizador, un dispositivo especial que empieza a vibrar y a
sonar cuando tu pedido está listo. De este modo, sabrás cuándo puedes ir a
recogerlo. Los localizadores son una analogía bastante exacta del concepto
promise.
Cuando haces un pedido, recoges el localizador y te sientas en tu mesa. Después,
puedes dedicarte a tus asuntos: navegar por Twitter, hablar por teléfono o leer un
libro. Cuando el localizador empieza a sonar, dejas lo que estás haciendo y vas a
por tu pedido.
Los promises son como los localizadores de JavaScript. Nos permiten describir un
código que no debe ejecutarse inmediatamente, sino solo después de que se
produzca un evento determinado.
Sintaxis
Los promises se utilizan a menudo para enviar peticiones a los dispositivos, como
los servidores. Una solicitud puede ser resolved (resuelta) o rejected (rechazada).
Por lo tanto, al crear un promise, tenemos que describir las instrucciones para dos
casos: cuando la solicitud se resuelve, y cuando se rechaza. Pero antes de
programar esta lógica, veamos cómo el motor determina si nuestra petición ha
sido procesada o no.
Para "enseñar" al motor a procesar las peticiones, pasamos una función al
constructor Promise(). Esta función de callback, llamada ejecutor, toma dos
callbacks como argumentos: resolve() y reject(). Estos callbacks cambian el estado
de promise a "resolved" o "rejected" desde su estado inicial de "pending"
(pendiente).
Copiar códigoJAVASCRIPT
if (rand) {
resolve("Solicitud procesada satisfactoriamente");
} else {
reject("Solicitud rechazada");
}
});
// crear un promise
const newPromise = new Promise(function(resolve, reject) {
// determina aleatoriamente si la solicitud ha sido procesada o no
const rand = Math.random() > 0.5 ? true : false;
if (rand) {
resolve("Solicitud procesada satisfactoriamente");
} else {
reject("Solicitud rechazada");
}
});
newPromise
.then(function (value) { // se ejecuta si promise se ha resuelto
console.log(value);
})
.catch(function (value) { // se ejecuta si promise ha sido rechazado
/* en este caso, el parámetro value almacena el valor
pasado al método reject(), es decir
el string "Solicitud rechazada" */
function firstAction(value) {
/* el parámetro value recibirá lo que le pasamos
al método resolve() al crear el promise,
es decir, el string "Un Mississippi" */
function secondAction(value) {
/* el valor será igual al valor retornado
por el método then() anterior, es decir, el string "Un Mississippi, dos Mississippis" */
function thirdAction(value) {
console.log(value);
}
newPromise.then(firstAction).then(secondAction).then(thirdAction);
newPromise
.then(firstAction)
.then(secondAction)
.then(thirdAction);
function firstAction(value) {
return `${value}, dos Mississippis`;
}
function secondAction(value) {
return `${value}, tres Mississippis`;
}
function thirdAction(value) {
console.log(value);
}
newPromise
.then(firstAction)
.then(secondAction)
.catch(thirdAction);
Ten en cuenta que el código de la función, que pasamos a los métodos catch() y
then(), también puede dar lugar a un error.
Termina siempre una cadena de promises con una llamada catch(). Esto
controlará el error si ocurre en cualquiera de las llamadas a then() en la cadena.
Métodos estáticos de promise
Promise() tiene métodos integrados. Resultan muy útiles a la hora de crear
promises. Ten en cuenta que los promises no tienen estos métodos; sólo la
función Promise() los tiene.
Promise.resolve() y Promise.reject()
No es necesario llamar a new Promise() cuando quieras crear inmediatamente un
promise resuelto o rechazado. Simplemente, puedes utilizar los métodos
Promise.resolve() y Promise.reject(). Estos métodos crean un promise, cambian su
estado a "resolved" o "rejected", respectivamente, y almacenan el valor que se
les pasa como resultado de este promise.
Copiar códigoJAVASCRIPT
Promise.all()
Puedes tener varios promises en tu página. Por ejemplo, solicitas una imagen de
un servidor y un texto escrito por otra persona para crear una entrada de un blog
compuesta por ambos. Este post solo sirve para su propósito si contiene tanto el
texto como la imagen, y uno no tiene sentido sin el otro. En otras palabras,
queremos crear la publicación después de que se resuelvan ambos promises.
Hay un método estático llamado Promise.all() que podemos utilizar para esto.
Toma un array de entrada con promises y ejecuta el código escrito en then() solo
cuando el estado de cada promise está "resolved":
Copiar códigoJAVASCRIPT
Como tal, los promises son peticiones al código asíncrono. Cuando creamos un
promise, lo que le pedimos al motor es: ejecuta este código y, en función de los
resultados, cambia el estado del promise a "resolved" o "rejected".
Todas las acciones posteriores con el resultado de la solicitud se escriben en la
cadena de métodos then() y catch(). Toman como argumento un callback con un
parámetro. Este parámetro recibe el valor que devolvió la función then() o catch()
anterior, o el valor con el que se llamó a la función resolve() o reject().