¿Alguna vez te has preguntado cómo los navegadores leen y ejecutan código JavaScript? Parece misterioso, pero en esta publicación puedes tener una idea de lo que está sucediendo bajo el capó.
Comenzamos nuestro viaje al idioma con una excursión al maravilloso mundo de los motores JavaScript.
Abra la consola en Chrome y vaya a la pestaña Fuentes. Verá varias secciones, y una de las más interesantes se llama
Pila de llamadas (en Firefox verá Pila de llamadas cuando coloque un punto de interrupción en el código):

¿Qué es una pila de llamadas? Parece que están sucediendo muchas cosas aquí, incluso por el simple hecho de ejecutar un par de líneas de código. De hecho, JavaScript no viene en una caja con cada navegador. Hay un gran componente que compila e interpreta nuestro código JavaScript: es un motor de JavaScript. Los más populares son V8, se usa en Google Chrome y Node.js, SpiderMonkey en Firefox, JavaScriptCore en Safari / WebKit.
Los motores JavaScript de hoy son excelentes ejemplos de ingeniería de software, y será casi imposible hablar sobre todos los aspectos. Sin embargo, el trabajo principal en la ejecución del código lo hacemos solo unos pocos componentes de los motores: Pila de llamadas (pila de llamadas), Memoria global (memoria global) y Contexto de ejecución (contexto de ejecución). Listo para conocerlos?
Contenido:
- Motores JavaScript y memoria global
- Motores JavaScript: ¿cómo funcionan? Contexto de ejecución global y pila de llamadas
- JavaScript es de un solo hilo y otras historias divertidas
- JavaScript asíncrono, cola de devolución de llamada y bucle de eventos
- Callback infierno y promesas ES6
- Crear y trabajar con JavaScript Promises
- Manejo de errores en promesas de ES6
- ES6 Combinadores de promesa: Promise.all, Promise.allSettled, Promise.any y otros
- Promesas de ES6 y cola de microtask
- Motores JavaScript: ¿cómo funcionan? Evolución asincrónica: de promesas a asíncrono / espera
- Motores JavaScript: ¿cómo funcionan? Resumen
1. Motores JavaScript y memoria global
Dije que JavaScript es un lenguaje compilado e interpretado. Lo creas o no, los motores de JavaScript compilan tus microsegundos de código antes de que se ejecute.
Algún tipo de magia, ¿eh? Esta magia se llama JIT (compilación Just in time). Solo es un gran tema de discusión, incluso los libros no serán suficientes para describir el trabajo de JIT. Pero por ahora, omitiremos la teoría y nos centraremos en la fase de ejecución, que no es menos interesante.
Para comenzar, mira este código:
var num = 2; function pow(num) { return num * num; }
¿Y si te pregunto cómo se procesa este código en un navegador? ¿Qué vas a responder? Puede decir: "el navegador lee el código" o "el navegador ejecuta el código". En realidad, no todo es tan simple. Primero, el código no lo lee el navegador, sino el motor.
El motor de JavaScript lee el código y, tan pronto como define la primera línea, coloca un par de enlaces en
la memoria global .
La memoria global (también llamada montón) es el área en la que el motor de JavaScript almacena variables y declaraciones de funciones. Y cuando lea el código anterior, aparecerán dos carpetas en la memoria global:

Incluso si el ejemplo contiene solo una variable y una función, imagine que su código JavaScript se ejecuta en un entorno más grande: en un navegador o en Node.js. En tales entornos, hay muchas funciones y variables predefinidas que se denominan globales. Por lo tanto, la memoria global contendrá muchos más datos que solo
num
y
pow
, tenga en cuenta.
Nada se está ejecutando en este momento. Ahora intentemos ejecutar nuestra función:
var num = 2; function pow(num) { return num * num; } pow(num);
Que va a pasar Y algo interesante sucederá. Al llamar a la función, el motor de JavaScript resaltará dos secciones:
- Contexto de ejecución global
- Pila de llamadas
Que son
2. Motores JavaScript: ¿cómo funcionan? Contexto de ejecución global y pila de llamadas
Aprendiste cómo el motor de JavaScript lee variables y declaraciones de funciones. Caen en la memoria global (montón).
Pero ahora estamos ejecutando una función de JavaScript, y el motor debería ocuparse de esto. Como? Cada motor de JavaScript tiene un
componente clave llamado pila de llamadas .
Esta es una estructura de datos apilados : se pueden agregar elementos desde arriba, pero no se pueden excluir de la estructura mientras haya otros elementos sobre ellos. Así es como funcionan las funciones de JavaScript. En la ejecución, no pueden abandonar la pila de llamadas si hay otra función presente en ella. Preste atención a esto, ya que este concepto ayuda a comprender la afirmación "JavaScript es de subproceso único".
Pero volvamos a nuestro ejemplo.
Cuando se llama a una función, el motor la envía a la pila de llamadas :

Me gusta presentar la pila de llamadas como una pila de chips Pringles. No podemos comer chips del fondo de la pila hasta que comamos los que están arriba. Afortunadamente, nuestra función es síncrona: es solo una multiplicación que se calcula rápidamente.
Al mismo tiempo, el motor coloca el
contexto de ejecución global en la memoria, este es el entorno global en el que se ejecuta el código JavaScript. Así es como se ve:

Imagine un contexto de ejecución global en forma de un mar en el que las funciones globales de JavaScript flotan como peces. Que dulce Pero esto es solo la mitad de la historia. ¿Qué pasa si nuestra función tiene variables anidadas o funciones internas?
Incluso en el caso simple, como se muestra a continuación, el motor de JavaScript crea un
contexto de ejecución local :
var num = 2; function pow(num) { var fixed = 89; return num * num; } pow(num);
Tenga en cuenta que agregué la variable
fixed
a la función
pow
. En este caso, el contexto de ejecución local contendrá una sección para
fixed
. No soy muy bueno dibujando rectángulos pequeños dentro de otros rectángulos pequeños, así que usa tu imaginación.
Aparecerá un contexto de ejecución local junto a
pow
, dentro de la sección del rectángulo verde ubicada dentro del contexto de ejecución global. Imagine también cómo para cada función anidada dentro de la función anidada, el motor crea otros contextos de ejecución local. ¡Todas estas secciones rectangulares aparecen muy rápidamente! ¡Como una muñeca que anida!
Volvamos a la historia de un solo hilo. ¿Qué significa esto?
3. JavaScript es de un solo hilo y otras historias divertidas
Decimos que
JavaScript es de un solo subproceso porque solo una pila de llamadas maneja nuestras funciones . Permítame recordarle que las funciones no pueden abandonar la pila de llamadas si otras funciones esperan ejecución.
Esto no es un problema si trabajamos con código síncrono. Por ejemplo, la suma de dos números es sincrónica y se calcula en microsegundos. ¿Qué pasa con las llamadas de red y otras interacciones con el mundo exterior?
Afortunadamente, los
motores de JavaScript están diseñados para funcionar de forma asíncrona de manera predeterminada . Incluso si pueden ejecutar solo una función a la vez, una entidad externa puede realizar funciones más lentas; en nuestro caso, es un navegador. Hablaremos de esto a continuación.
Al mismo tiempo, sabe que cuando el navegador carga algún tipo de código JavaScript, el motor lee este código línea por línea y realiza los siguientes pasos:
- Pone variables y declaraciones de funciones en la memoria global (montón).
- Envía una llamada a cada función en la pila de llamadas.
- Crea un contexto de ejecución global en el que se ejecutan funciones globales.
- Crea muchos pequeños contextos de ejecución local (si hay variables internas o funciones anidadas).
Ahora tiene una comprensión básica de la mecánica de sincronización que subyace a todos los motores de JavaScript. En el próximo capítulo, hablaremos sobre cómo funciona el código asincrónico en JavaScript y por qué funciona de esa manera.
4. JavaScript asíncrono, cola de devolución de llamada y bucle de eventos
Gracias a la memoria global, el contexto de ejecución y la pila de llamadas, el código JavaScript síncrono se ejecuta en nuestros navegadores. Pero olvidamos algo. ¿Qué sucede si necesita ejecutar algún tipo de función asincrónica?
Por función asincrónica, me refiero a cada interacción con el mundo exterior, lo que puede llevar algún tiempo completar. Llamar a la API REST o al temporizador es asíncrono, porque puede llevar segundos ejecutarlos. Gracias a los elementos disponibles en el motor, podemos procesar tales funciones sin bloquear la pila de llamadas y el navegador. No olvide que la pila de llamadas puede ejecutar solo una función a la vez, e
incluso una función de bloqueo puede detener literalmente el navegador . Afortunadamente, los motores de JavaScript son inteligentes y, con un poco de ayuda del navegador, pueden resolver las cosas.
Cuando ejecutamos una función asincrónica, el navegador la toma y la realiza por nosotros. Toma un temporizador como este:
setTimeout(callback, 10000); function callback(){ console.log('hello timer!'); }
Estoy seguro de que, aunque ya haya visto
setTimeout
cientos de veces, es posible que no sepa que
esta función no está integrada en JavaScript . Entonces, cuando apareció JavaScript, no había ninguna función
setTimeout
en él. De hecho, es parte de las llamadas API de navegador, una colección de herramientas convenientes que nos proporciona el navegador. Maravilloso! Pero, ¿qué significa esto en la práctica? Dado que
setTimeout
refiere a la API del navegador, esta función la ejecuta el propio navegador (por un momento aparece en la pila de llamadas, pero se elimina inmediatamente de allí).
Después de 10 segundos, el navegador toma la función de devolución de llamada que le pasamos y la coloca en la
cola de devolución de llamada . Por el momento, dos secciones rectangulares más han aparecido en el motor de JavaScript. Echa un vistazo a este código:
var num = 2; function pow(num) { return num * num; } pow(num); setTimeout(callback, 10000); function callback(){ console.log('hello timer!'); }
Ahora nuestro esquema se ve así:

setTimeout
se ejecuta dentro del contexto del navegador. Después de 10 segundos, se inicia el temporizador y la función de devolución de llamada está lista para su ejecución. Pero primero, debe pasar por la cola de devolución de llamada. Esta es una estructura de datos en forma de una cola y, como su nombre lo indica, es una cola ordenada de funciones.
Cada función asincrónica debe pasar por una cola de devolución de llamada antes de llegar a la pila de llamadas. ¿Pero quién envía las funciones a continuación? Esto hace que un componente llamado
un bucle de eventos .
Hasta ahora, el bucle de eventos solo trata una cosa: comprueba si la pila de llamadas está vacía. Si hay alguna función en la cola de devolución de llamada y si la pila de llamadas está libre, es hora de enviar una devolución de llamada a la pila de llamadas.
Después de eso, la función se considera ejecutada. Este es el esquema general para procesar código asíncrono y síncrono con el motor de JavaScript:

Digamos que
callback()
está listo para ejecutarse. Cuando
pow()
la pila de llamadas se libera y el bucle de eventos le envía callback()
. ¡Y eso es todo! Aunque simplifiqué un poco las cosas, si comprende el diagrama anterior, puede comprender todo JavaScript.
Recuerde:
las API basadas en navegador, las colas de devolución de llamada y los bucles de eventos son los pilares de JavaScript asíncrono .
Y si está interesado, puede ver el curioso video "Qué diablos es el ciclo de eventos de todos modos", de Philip Roberts. Esta es una de las mejores explicaciones para el bucle de eventos.
Pero todavía no hemos terminado con el tema asincrónico de JavaScript. En los siguientes capítulos consideraremos las promesas de ES6.
5. Callback hell y promesas de ES6
Las funciones de devolución de llamada se utilizan en JavaScript en todas partes, tanto en código síncrono como asíncrono. Considere este método:
function mapper(element){ return element * 2; } [1, 2, 3, 4, 5].map(mapper);
mapper
es una función de devolución de llamada que se pasa dentro del
map
. El código anterior es síncrono. Ahora considere este intervalo:
function runMeEvery(){ console.log('Ran!'); } setInterval(runMeEvery, 5000);
Este código es asíncrono, porque dentro de
setInterval
pasamos la devolución de llamada runMeEvery. Las devoluciones de llamada se utilizan en todo JavaScript, por lo que durante años hemos tenido un problema llamado "infierno de devolución de llamada" - "infierno de devolución de llamada".
El término
infierno de devolución de llamada en JavaScript se aplica al "estilo" de programación, en el que las devoluciones de llamada están incrustadas en otras devoluciones de llamada que están incrustadas en otras devoluciones de llamada ... Debido a la naturaleza asincrónica, los programadores de JavaScript han caído en esta trampa.
Para ser honesto, nunca creé grandes pirámides de devoluciones de llamada. Quizás porque valoro el código legible y siempre trato de mantener sus principios. Si golpea el infierno de devolución de llamada, significa que su función hace demasiado.
No voy a hablar en detalle sobre el infierno de devolución de llamada, si está interesado, vaya a
callbackhell.com , donde este problema ha sido investigado en detalle y se han propuesto varias soluciones. Y hablaremos sobre las
promesas de ES6 . Este es un complemento de JavaScript diseñado para resolver el infierno problema de devolución de llamada. ¿Pero qué son las promesas?
Una promesa de JavaScript es una representación de un evento futuro . Una promesa puede terminar con éxito, o en una jerga de programadores, una promesa será "resuelta" (resuelta). Pero si la promesa termina con un error, entonces decimos que está en el estado rechazado. Las promesas también tienen un estado predeterminado: cada nueva promesa comienza en un estado pendiente. ¿Puedo crear mi propia promesa? Si Hablaremos de esto en el próximo capítulo.
6. Crear y trabajar con promesas de JavaScript
Para crear una nueva promesa, debe llamar al constructor pasándole una función de devolución de llamada. Solo puede tomar dos parámetros:
resolve
y
reject
. Creemos una nueva promesa que se resolverá en 5 segundos (puede probar los ejemplos en la consola del navegador):
const myPromise = new Promise(function(resolve){ setTimeout(function(){ resolve() }, 5000) });
Como puede ver,
resolve
es una función que llamamos para que la promesa finalice con éxito. Y
reject
creará una promesa rechazada:
const myPromise = new Promise(function(resolve, reject){ setTimeout(function(){ reject() }, 5000) });
Tenga en cuenta que puede ignorar el
reject
porque este es el segundo parámetro. Pero si tiene la intención de utilizar el
reject
, no
puede ignorar la resolve
. Es decir, el siguiente código no funcionará y terminará con una promesa permitida:
Las promesas no parecen tan útiles en este momento, ¿verdad? Estos ejemplos no muestran nada al usuario. Agreguemos algo. Y las promesas permitidas y rechazadas pueden devolver datos. Por ejemplo:
const myPromise = new Promise(function(resolve) { resolve([{ name: "Chris" }]); });
Pero todavía no vemos nada.
Para extraer datos de una promesa, debe asociar la promesa con el método then
. Él toma una devolución de llamada (¡qué ironía!), Que recibe los datos actuales:
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then(function(data) { console.log(data); });
Como desarrollador de JavaScript y consumidor del código de otras personas, en su mayoría interactúa con promesas externas. Los creadores de la biblioteca a menudo envuelven el código heredado en un constructor de Promise, como este:
const shinyNewUtil = new Promise(function(resolve, reject) {
Y si es necesario, también podemos crear y resolver una promesa llamando a
Promise.resolve()
:
Promise.resolve({ msg: 'Resolve!'}) .then(msg => console.log(msg));
Entonces, permíteme recordarte: las promesas de JavaScript son un marcador para un evento que sucederá en el futuro. Un evento comienza en el estado "esperando una decisión" y puede ser exitoso (permitido, ejecutado) o no exitoso (rechazado). Una promesa puede devolver datos que se pueden recuperar al adjuntarlos. En el próximo capítulo, discutiremos cómo lidiar con los errores que provienen de las promesas.
7. Manejo de errores en promesas de ES6
El manejo de errores en JavaScript siempre fue fácil, al menos en código síncrono. Echa un vistazo a un ejemplo:
function makeAnError() { throw Error("Sorry mate!"); } try { makeAnError(); } catch (error) { console.log("Catching the error! " + error); }
El resultado será:
Catching the error! Error: Sorry mate!
Como se esperaba, el error cayó en el
catch
. Ahora prueba la función asincrónica:
function makeAnError() { throw Error("Sorry mate!"); } try { setTimeout(makeAnError, 5000); } catch (error) { console.log("Catching the error! " + error); }
Este código es asíncrono debido a
setTimeout
. ¿Qué pasará si lo ejecutamos?
throw Error("Sorry mate!"); ^ Error: Sorry mate! at Timeout.makeAnError [as _onTimeout] (/home/valentino/Code/piccolo-javascript/async.js:2:9)
Ahora el resultado es diferente. El error no fue detectado por el
catch
, sino que se elevó libremente en la pila. La razón es que
try/catch
solo funciona con código síncrono. Si desea saber más, este problema se trata en detalle
aquí .
Afortunadamente, con promesas, podemos manejar los errores asincrónicos como si fueran sincrónicos. En el último capítulo, dije que llamar a
reject
conduce a un rechazo de la promesa:
const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); });
En este caso, podemos manejar los errores utilizando el controlador de
catch
tirando (nuevamente) una devolución de llamada:
const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); }); myPromise.catch(err => console.log(err));
Además, para crear y rechazar una promesa en el lugar correcto, puede llamar a
Promise.reject()
:
Promise.reject({msg: 'Rejected!'}).catch(err => console.log(err));
Permítame recordarle: el
then
controlador se ejecuta cuando se ejecuta la promesa, y el controlador de
catch
se ejecuta para las promesas rechazadas. Pero este no es el final de la historia. A continuación veremos cómo
async/await
funciona muy bien con
try/catch
.
8. Combinadores de promesas de ES6: Promise.all, Promise.allSettled, Promise.any y otros
Las promesas no están diseñadas para funcionar solas. Promise API ofrece varios métodos para
combinar promesas . Una de las más útiles
es Promise.all , toma una matriz de promesas y devuelve una promesa. El único problema es que Promise.all se rechaza si se rechaza al menos una promesa de la matriz.
Promise.race permite o rechaza tan pronto como una de las promesas de la matriz recibe el estado correspondiente.
En versiones más recientes de V8, también se introducirán dos nuevos combinadores:
Promise.allSettled
y
Promise.any
.
Promise.any aún se encuentra en una etapa temprana de la funcionalidad propuesta, al momento de escribir este artículo no es compatible. Sin embargo, en teoría, podrá indicar si se ha ejecutado alguna promesa. La diferencia con
Promise.race
es que
Promise.any no se rechaza, incluso si se rechaza una de las promesas .
Promise.allSettled
aún más interesante. También toma una serie de promesas, pero no se "acorta" si una de las promesas es rechazada. Es útil cuando necesita verificar si todas las promesas en una matriz han pasado a alguna etapa, independientemente de la presencia de promesas rechazadas. Se puede considerar lo contrario de
Promise.all
.
9. Promesas de ES6 y la cola de microtask
Si recuerda del capítulo anterior, cada función de devolución de llamada asíncrona en JavaScript está en la cola de devolución de llamada antes de que llegue a la pila de llamadas. Pero las funciones de devolución de llamada pasadas a Promise tienen un destino diferente: son procesadas por la cola de microtask, en lugar de la cola de tareas.
Y aquí debe tener cuidado: la
cola de microtask precede a la cola de llamadas . Las devoluciones de llamada de la cola de microtask tienen prioridad cuando el bucle de eventos verifica si hay nuevas devoluciones de llamadas listas para la pila de llamadas.
Jake Archibald describe esta mecánica con más detalle en
Tareas, microtasks, colas y horarios , excelente lectura.
10. Motores JavaScript: ¿cómo funcionan? Evolución asincrónica: de promesas a asíncrono / espera
JavaScript está evolucionando rápidamente y constantemente estamos recibiendo mejoras cada año. Las promesas parecían un final, pero
con ECMAScript 2017 (ES8) apareció una nueva sintaxis: async/await
.
async/await
es solo una mejora estilística que llamamos azúcar sintáctico.
async/await
no cambia JavaScript de ninguna manera (no olvide que el idioma debe ser compatible con versiones anteriores de los navegadores antiguos y no debe romper el código existente). Esta es solo una nueva forma de escribir código asincrónico basado en promesas. Considera un ejemplo. Arriba, ya guardamos la promesa en el correspondiente
then
:
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); myPromise.then((data) => console.log(data))
Ahora
con async/await
podemos procesar el código asincrónico para que para el lector de nuestra lista el código se vea sincrónico . En lugar de usar
then
podemos envolver la promesa en una función etiquetada como
async
, y luego
await
resultado:
const myPromise = new Promise(function(resolve, reject) { resolve([{ name: "Chris" }]); }); async function getData() { const data = await myPromise; console.log(data); } getData();
Se ve bien, ¿verdad? Es curioso que una función asincrónica siempre devuelva una promesa, y nadie puede evitar que haga esto:
async function getData() { const data = await myPromise; return data; } getData().then(data => console.log(data));
¿Qué hay de los errores? Una de las ventajas de
async/await
es que esta construcción puede permitirnos usar
try/catch
. Lea la
introducción al manejo de errores en las funciones asíncronas y sus pruebas .
Echemos un vistazo a la promesa nuevamente, en la que manejamos los errores con el controlador
catch
:
const myPromise = new Promise(function(resolve, reject) { reject('Errored, sorry!'); }); myPromise.catch(err => console.log(err));
Con funciones asincrónicas, podemos refactorizar así:
async function getData() { try { const data = await myPromise; console.log(data);
Sin embargo, no todos han cambiado a este estilo.
try/catch
puede complicar su código. Hay una cosa más a tener en cuenta. Vea cómo se produce un error dentro de este bloque de
try
en este código:
async function getData() { try { if (true) { throw Error("Catch me if you can"); } } catch (err) { console.log(err.message); } } getData() .then(() => console.log("I will run no matter what!")) .catch(() => console.log("Catching err"));
¿Qué pasa con las dos líneas que se muestran en la consola? Recuerde que
try/catch
es una construcción sincrónica, y nuestra función asincrónica genera una promesa . Siguen dos caminos diferentes, como los trenes. ! ,
throw
,
catch
getData()
. , «Catch me if you can», «I will run no matter what!».
,
throw
then
. , ,
Promise.reject()
:
async function getData() { try { if (true) { return Promise.reject("Catch me if you can"); } } catch (err) { console.log(err.message); } } Now the error will be handled as expected: getData() .then(() => console.log("I will NOT run no matter what!")) .catch(() => console.log("Catching err")); "Catching err"
async/await
JavaScript. .
, JS-
async/await
. . ,
async/await
— .
11. JavaScript-: ?
JavaScript — , , . JS-: V8, Google Chrome Node.js; SpiderMonkey, Firefox; JavaScriptCore, Safari.
JavaScript- «» : , , , . , .
JavaScript- , . JavaScript: , - , (, ) .
ECMAScript 2015 . — , . . 2017-
async/await
: , , .