¿Por qué mi variable no se modifica después de modificarla dentro de una función? - Referencia de código asincrónico
Dados los siguientes ejemplos, ¿por qué outerScopeVar
no está definido en todos los casos?
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
outerScopeVar = response;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
¿Por qué aparece undefined
en todos estos ejemplos? No quiero soluciones alternativas, quiero saber por qué sucede esto.
Nota: esta es una pregunta canónica para la asincronicidad de JavaScript . Siéntase libre de mejorar esta pregunta y agregar ejemplos más simplificados con los que la comunidad se pueda identificar.
Respuestas
Respuesta de una palabra: asincronicidad .
Prólogo
Este tema se ha repetido 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 de @Felix Kling a "¿Cómo devuelvo la respuesta de una llamada asincrónica?" . Vea su excelente respuesta que explica los flujos sincrónicos y asincrónicos, así como la sección "Reestructurar el código".
@Benjamin Gruenbaum también se ha esforzado mucho en explicar la asincronicidad en el mismo hilo.La respuesta de @Matt Esch a "Obtener datos de fs.readFile" también explica la asincronía extremadamente bien de una manera sencilla.
La respuesta a la pregunta que nos ocupa
Primero rastreemos el comportamiento común. En todos los ejemplos, outerScopeVar
se modifica dentro de una función . Esa función claramente no se ejecuta de inmediato, se asigna o pasa como argumento. Eso es lo que llamamos 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 el retraso haya expirado y el tiempo de espera no haya sido cancelado porclearTimeout
. Nota: incluso cuando se usa0
como retraso, todos los navegadores tienen un límite de retraso de tiempo de espera mínimo (especificado en 4 ms en la especificación HTML5).$.post
La devolución de llamada de jQuery puede ser llamada en algún momento en el futuro , cuando (y si) la solicitud 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 arroje un error.
En todos los casos, tenemos una devolución de llamada que puede ejecutarse en el futuro . Este "en algún momento en el futuro" es lo que llamamos flujo asincrónico .
La ejecución asincrónica se elimina del flujo sincrónico. Es decir, el código asincrónico nunca se ejecutará mientras se esté ejecutando la pila de código sincrónico. Este es el significado de JavaScript de un solo subproceso.
Más específicamente, cuando el motor JS está inactivo, sin ejecutar una pila de (a) código síncrono, sondeará eventos que pueden haber desencadenado devoluciones de llamada asíncronas (por ejemplo, tiempo de espera expirado, respuesta de red recibida) y los ejecutará uno tras otro. Esto se considera un bucle de eventos .
Es decir, el código asíncrono 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 de forma sincrónica pero se ejecutan de forma asincrónica. Simplemente no puede confiar en la ejecución de una función asíncrona hasta que sepa que se ha ejecutado, y ¿cómo hacerlo?
Realmente es simple. La lógica que depende de la ejecución de la función asincrónica debe iniciarse / llamarse desde dentro de esta función asincrónica. Por ejemplo, mover los alert
s y console.log
demasiado dentro de la función de devolución de llamada emitiría el resultado esperado, ya que el resultado está disponible en ese momento.
Implementando su propia lógica de devolución de llamada
A menudo es necesario hacer más cosas con el resultado de una función asíncrona o hacer cosas diferentes con el resultado dependiendo de dónde se haya llamado a la función asincrónica. Abordemos 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érica, 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 espera hasta que se ejecute la función asincrónica.
Abordémoslo implementando nuestro propio sistema de devolución de llamada. 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);
}
La mayoría de las veces, en casos de uso reales, 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 síncrono y reestructurar su código para adaptarse a eso.
También notará que debido a la naturaleza asíncrona, es imposible return
devolver un valor de un flujo asíncrono al flujo síncrono 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, tendrá que hacer uso del patrón de devolución de llamada, o ... Promesas.
Promesas
Aunque hay formas de mantener a raya el infierno de las devoluciones de llamada con Vanilla JS, las promesas están ganando popularidad y actualmente se están estandarizando en ES6 (ver Promise - MDN ).
Promises (también conocido como Futures) proporcionan una lectura más lineal y, por lo tanto, agradable del código asincrónico, pero explicar toda su funcionalidad está fuera del alcance de esta pregunta. En cambio, dejaré estos excelentes recursos para los interesados:
Más material de lectura sobre la asincronicidad de JavaScript
- The Art of Node - Callbacks explica muy bien el código asincrónico y las devoluciones de llamada con ejemplos de vainilla JS y código Node.js también.
Nota: He marcado esta respuesta como Wiki de la comunidad, por lo que cualquier persona con al menos 100 reputaciones puede editarla y mejorarla. No dude en mejorar esta respuesta o envíe una respuesta completamente nueva si también lo desea.
Quiero convertir esta pregunta en un tema canónico para responder problemas de asincronicidad 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 lo mejor y útil posible. !
La respuesta de Fabrício es acertada; pero quería complementar su respuesta con algo menos técnico, que se centra en una analogía para ayudar a explicar el concepto de asincronicidad .
Una analogía ...
Ayer, el trabajo que estaba haciendo requería información de un colega. Le llamé; así es como fue la conversación:
Me : Hola Bob, necesito saber cómo foo 'd la barra de ' D la semana pasada. Jim quiere un informe al respecto y tú eres el único que conoce los detalles.
Bob : Claro, pero ¿me llevará unos 30 minutos?
Yo : Eso es genial Bob. ¡Devuélveme la llamada cuando tengas la información!
En este punto, colgué el teléfono. Como necesitaba información de Bob para completar mi informe, dejé el informe y fui a tomar un café, luego me puse al día con un correo electrónico. 40 minutos después (Bob es lento), Bob volvió a llamar y me dio la información que necesitaba. En este punto, reanudé mi trabajo con mi informe, ya que tenía toda la información que necesitaba.
Imagínese si la conversación hubiera sido así;
Me : Hola Bob, necesito saber cómo foo 'd la barra de ' D la semana pasada. Jim quiere un informe al respecto y tú eres el único que conoce los detalles.
Bob : Claro, pero ¿me llevará unos 30 minutos?
Yo : Eso es genial Bob. Esperaré.
Y me senté allí y esperé. Y esperó. Y esperó. Por 40 minutos. Sin hacer nada más que esperar. Finalmente, Bob me dio la información, colgamos y completé mi informe. Pero había perdido 40 minutos de productividad.
Este es un comportamiento asíncrono frente a síncrono
Esto es exactamente lo que está sucediendo en todos los ejemplos de nuestra pregunta. Cargar una imagen, cargar un archivo fuera del disco y solicitar una página a través de AJAX son todas operaciones lentas (en el contexto de la informática moderna).
En lugar de esperar a que se completen estas operaciones lentas, JavaScript le permite registrar una función de devolución de llamada que se ejecutará cuando se complete la operación lenta. Mientras tanto, sin embargo, JavaScript seguirá ejecutando otro código. El hecho de que JavaScript ejecute otro código mientras espera que se complete la operación lenta hace que el comportamiento sea asincrónico . Si JavaScript hubiera esperado a que se completara la operación antes de ejecutar cualquier otro código, este habría sido un comportamiento sincrónico .
var outerScopeVar;
var img = document.createElement('img');
// Here we register the callback function.
img.onload = function() {
// Code within this function will be executed once the image has loaded.
outerScopeVar = this.width;
};
// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);
En el código anterior, le pedimos a JavaScript que se cargue lolcat.png
, lo cual es una operación lenta . La función de devolución de llamada se ejecutará una vez que se haya realizado esta operación lenta, pero mientras tanto, JavaScript seguirá procesando las siguientes líneas de código; es decir alert(outerScopeVar)
.
Por eso vemos que se muestra la alerta undefined
; ya que alert()
se procesa inmediatamente, en lugar de después de que se haya cargado la imagen.
Para arreglar nuestro código, todo lo que tenemos que hacer es mover el alert(outerScopeVar)
código a la función de devolución de llamada. Como consecuencia de esto, ya no necesitamos la outerScopeVar
variable declarada como variable global.
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Usted siempre ve una devolución de llamada se especifica como una función, porque esa es la única manera * en JavaScript para definir un código, pero no lo ejecuta hasta más tarde.
Por lo tanto, en todos nuestros ejemplos, function() { /* Do something */ }
es la devolución de llamada; para arreglar todos los ejemplos, ¡todo lo que tenemos que hacer es mover allí el código que necesita la respuesta de la operación!
* Técnicamente también puedes usarlo eval()
, pero eval()
es malo para este propósito
¿Cómo hago esperar a mi interlocutor?
Es posible que actualmente tenga algún código similar a este;
function getWidthOfImage(src) {
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = src;
return outerScopeVar;
}
var width = getWidthOfImage('lolcat.png');
alert(width);
Sin embargo, ahora sabemos que return outerScopeVar
ocurre de inmediato; antes de que la onload
función de devolución de llamada haya actualizado la variable. Esto lleva a getWidthOfImage()
regresar undefined
y a undefined
ser alertado.
Para solucionar esto, necesitamos permitir que la función que llama getWidthOfImage()
registre una devolución de llamada, luego mover la alerta del ancho para que esté dentro de esa devolución de llamada;
function getWidthOfImage(src, cb) {
var img = document.createElement('img');
img.onload = function() {
cb(this.width);
};
img.src = src;
}
getWidthOfImage('lolcat.png', function (width) {
alert(width);
});
... como antes, tenga en cuenta que hemos podido eliminar las variables globales (en este caso width
).
Aquí hay una respuesta más concisa para las personas que buscan una referencia rápida, así como algunos ejemplos que usan promesas y async / await.
Comience con el enfoque ingenuo (que no funciona) para una función que llama a un método asincrónico (en este caso setTimeout
) y devuelve un mensaje:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
se registra en este caso porque getMessage
regresa antes de que setTimeout
se llame y se actualice la devolución de llamada outerScopeVar
.
Las dos formas principales de resolverlo son mediante devoluciones de llamada y promesas :
Devoluciones de llamada
El cambio aquí es que getMessage
acepta un callback
parámetro que se llamará para devolver los resultados al código de llamada una vez que esté disponible.
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Las promesas proporcionan una alternativa que es más flexible que las devoluciones de llamada porque se pueden combinar de forma natural para coordinar múltiples operaciones asíncronas. Un Promesas / A + implementación estándar se proporciona en forma nativa Node.js (0.12+) y muchos navegadores actuales, sino que también se implementa en bibliotecas como Bluebird y Q .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
aplazamientos de jQuery
jQuery proporciona una funcionalidad similar a las promesas con sus diferidos.
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
async / await
Si su entorno de JavaScript incluye soporte para async
y await
(como Node.js 7.6+), entonces puede usar promesas sincrónicamente dentro de las async
funciones:
function getMessage () {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
async function main() {
let message = await getMessage();
console.log(message);
}
main();
Para decir lo obvio, la copa representa outerScopeVar
.
Las funciones asincrónicas son como ...
Las otras respuestas son excelentes y solo quiero dar una respuesta directa a esto. Solo limitando a llamadas asincrónicas de jQuery
Todas las llamadas ajax (incluidas las $.get
or $.post
o $.ajax
) son asincrónicas.
Considerando tu ejemplo
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
La ejecución del código comienza desde la línea 1, declara la variable y dispara una llamada asincrónica en la línea 2, (es decir, la solicitud de publicación) y continúa su ejecución desde la línea 3, sin esperar a que la solicitud de publicación complete su ejecución.
Digamos que la solicitud de publicación tarda 10 segundos en completarse, el valor de outerScopeVar
solo se establecerá después de esos 10 segundos.
Para probar,
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2, takes 10 seconds to complete
outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun"); //line 3
alert(outerScopeVar); //line 4
Ahora, cuando ejecute esto, recibirá una alerta en la línea 3. Ahora espere un tiempo hasta que esté seguro de que la solicitud de publicación ha devuelto algún valor. Luego, cuando haga clic en Aceptar, en el cuadro de alerta, la siguiente alerta imprimirá el valor esperado, porque lo esperaba.
En el escenario de la vida real, el código se convierte en,
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Todo el código que depende de las llamadas asincrónicas, se mueve dentro del bloque asincrónico o esperando las llamadas asincrónicas.
En todos estos escenarios outerScopeVar
se modifica o se le asigna un valor de forma asincrónica o sucediendo en un tiempo posterior (esperando o escuchando que ocurra algún evento), por lo cual la ejecución actual no esperará .outerScopeVar = undefined
Analicemos cada uno de los ejemplos (marqué la parte que se llama asincrónicamente o retrasada para que ocurran algunos eventos):
1.
Aquí registramos un listador de eventos que se ejecutará en ese evento en particular. Aquí cargando la imagen. Luego, la ejecución actual continúa con las siguientes líneas img.src = 'lolcat.png';
y, alert(outerScopeVar);
mientras tanto, el evento puede no ocurrir. es decir, la función img.onload
espera a que se cargue la imagen referida, de forma asincrónica. Esto sucederá con el siguiente ejemplo: el evento puede diferir.
2.
Aquí el evento de tiempo de espera juega el papel, que invocará al controlador después del tiempo especificado. Aquí está 0
, pero aún así registra un evento asíncrono se agregará a la última posición de la Event Queue
ejecución, lo que hace el retraso garantizado.
3.
4.
Node puede considerarse como el rey de la codificación asincrónica, aquí la función marcada se registra como un controlador de devolución de llamada que se ejecutará después de leer el archivo especificado.
5.
La promesa obvia (algo se hará en el futuro) es asincrónica. consulte ¿Cuáles son las diferencias entre Diferido, Promesa y Futuro en JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript