Respuesta de una palabra: asincronía .
Prefacio
Este tema se ha iterado al menos un par de miles de veces, aquí, en Stack Overflow. Por lo tanto, en primer lugar, me gustaría señalar algunos recursos extremadamente útiles:
La respuesta a la pregunta en cuestión.
Tracemos primero el comportamiento común. En todos los ejemplos, outerScopeVar
se modifica dentro de una función . Es evidente que esa función no se ejecuta de inmediato, se asigna o se pasa como argumento. Eso es lo que llamamos una devolución de llamada .
Ahora la pregunta es, ¿cuándo se llama esa devolución de llamada?
Depende del caso. Intentemos rastrear un comportamiento común nuevamente:
img.onload
se puede llamar en algún momento en el futuro , cuando (y si) la imagen se haya cargado correctamente.
setTimeout
se puede llamar en algún momento en el futuro , después de que la demora haya expirado y el tiempo de espera no haya sido cancelado clearTimeout
. Nota: incluso cuando se usa 0
como retraso, todos los navegadores tienen un límite de tiempo de espera mínimo (especificado en 4 ms en la especificación HTML5).
$.post
La devolución de llamada de jQuery se puede llamar en algún momento en el futuro , cuando (y si) la solicitud de Ajax se haya completado con éxito.
fs.readFile
Se puede llamar a Node.js en algún momento en el futuro , cuando el archivo se haya leído correctamente o se haya producido un error.
En todos los casos, tenemos una devolución de llamada que puede ejecutarse en algún momento en el futuro . Este "en algún momento en el futuro" es lo que llamamos flujo asíncrono .
La ejecución asincrónica se elimina del flujo sincrónico. Es decir, el código asincrónico nunca se ejecutará mientras se ejecuta la pila de código síncrono. Este es el significado de JavaScript con un solo subproceso.
Más específicamente, cuando el motor JS está inactivo, sin ejecutar una pila de (a) código síncrono, sondeará los eventos que pueden haber desencadenado devoluciones de llamada asincrónicas (por ejemplo, tiempo de espera expirado, respuesta de red recibida) y las ejecutará una tras otra. Esto se considera como Event Loop .
Es decir, el código asincrónico resaltado en las formas rojas dibujadas a mano puede ejecutarse solo después de que se haya ejecutado todo el código síncrono restante en sus respectivos bloques de código:
En resumen, las funciones de devolución de llamada se crean sincrónicamente pero se ejecutan de forma asincrónica. Simplemente no puede confiar en la ejecución de una función asincrónica hasta que sepa que se ha ejecutado, y ¿cómo hacer eso?
Es simple, de verdad. La lógica que depende de la ejecución de la función asincrónica debe iniciarse / invocarse desde esta función asincrónica. Por ejemplo, mover los alert
sys console.log
también dentro de la función de devolución de llamada generaría el resultado esperado, porque el resultado está disponible en ese punto.
Implementando su propia lógica de devolución de llamada
A menudo necesita hacer más cosas con el resultado de una función asincrónica o hacer cosas diferentes con el resultado dependiendo de dónde se haya llamado la función asincrónica. Veamos un ejemplo un poco más complejo:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
Nota: estoy usando setTimeout
con un retraso aleatorio como una función asíncrona genérico, el mismo ejemplo se aplica a Ajax, readFile
, onload
y cualquier otro flujo asíncrono.
Este ejemplo claramente sufre el mismo problema que los otros ejemplos, no está esperando hasta que se ejecute la función asincrónica.
Vamos a abordarlo implementando un sistema de devolución de llamada propio. En primer lugar, nos deshacemos de ese feo outerScopeVar
que es completamente inútil en este caso. Luego agregamos un parámetro que acepta un argumento de función, nuestra devolución de llamada. Cuando finaliza la operación asincrónica, llamamos a esta devolución de llamada pasando el resultado. La implementación (lea los comentarios en orden):
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
alert(result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
// 3. Start async operation:
setTimeout(function() {
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Fragmento de código del ejemplo anterior:
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
console.log("5. result is: ", result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
console.log("2. callback here is the function passed as argument above...")
// 3. Start async operation:
setTimeout(function() {
console.log("3. start async operation...")
console.log("4. finished async operation, calling the callback, passing the result...")
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Con mayor frecuencia en casos de uso real, la API DOM y la mayoría de las bibliotecas ya proporcionan la funcionalidad de devolución de llamada (la helloCatAsync
implementación en este ejemplo demostrativo). Solo necesita pasar la función de devolución de llamada y comprender que se ejecutará fuera del flujo sincrónico, y reestructurar su código para acomodarlo.
También notará que debido a la naturaleza asincrónica, es imposible que return
un valor de un flujo asincrónico regrese al flujo sincrónico donde se definió la devolución de llamada, ya que las devoluciones de llamada asincrónicas se ejecutan mucho después de que el código sincrónico ya haya terminado de ejecutarse.
En lugar de obtener return
un valor de una devolución de llamada asincrónica, deberá utilizar el patrón de devolución de llamada o ... Promesas.
Promesas
Aunque hay formas de mantener a raya el infierno de devolución de llamadas con vanilla JS, las promesas están creciendo en popularidad y actualmente se están estandarizando en ES6 (ver Promesa - MDN ).
Las promesas (también conocidas como futuros) proporcionan una lectura más lineal, y por lo tanto agradable, del código asincrónico, pero explicar su funcionalidad completa está fuera del alcance de esta pregunta. En cambio, dejaré estos excelentes recursos para los interesados:
Más material de lectura sobre asincronía de JavaScript
- The Art of Node - Callbacks explica muy bien el código asíncrono y las devoluciones de llamada con ejemplos de Vanilla JS y el código Node.js también.
Nota: He marcado esta respuesta como Community Wiki, por lo tanto, ¡cualquier persona con al menos 100 reputaciones puede editarla y mejorarla! Por favor, siéntase libre de mejorar esta respuesta, o envíe una respuesta completamente nueva si así lo desea.
Quiero convertir esta pregunta en un tema canónico para responder a problemas de asincronía que no están relacionados con Ajax (hay cómo devolver la respuesta de una llamada AJAX para eso), por lo tanto, este tema necesita su ayuda para ser tan bueno y útil como sea posible !