Pregunta ¿Por qué mi variable no se altera después de que la modifique dentro de una función? - Referencia de código asíncrono


Teniendo en cuenta los siguientes ejemplos, ¿por qué es outerScopeVar indefinido 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é sale undefined en todos estos ejemplos? No quiero soluciones, quiero saber por qué esto está ocurriendo.


Nota: Esta es una pregunta canónica para Asincronicidad de JavaScript. Siéntase libre de mejorar esta pregunta y agregar ejemplos más simplificados con los que la comunidad pueda identificarse.


543
2018-05-14 23:55


origen


Respuestas:


Respuesta de una palabra: asincronicidad.

Préstamos

Este tema se ha repetido al menos un par de miles de veces, aquí, en Stack Overflow. Por lo tanto, primero me gustaría señalar algunos recursos extremadamente útiles:


La respuesta a la pregunta en cuestión

Vamos a rastrear el comportamiento común primero. En todos los ejemplos, el outerScopeVar se modifica dentro de una función. Esa función claramente no se ejecuta inmediatamente, se está asignando o pasando como un argumento. Eso es lo que llamamos un llamar de vuelta.

Ahora la pregunta es, ¿cuándo se llama esa devolución de llamada?

Depende del caso Tratemos de rastrear algún comportamiento común de nuevo:

  • img.onload puede ser llamado algún día en el futuro, cuando (y si) la imagen se ha cargado correctamente.
  • setTimeout puede ser llamado algún día en el futuro, después de que la demora haya expirado y el tiempo de espera no haya sido cancelado por clearTimeout. Nota: incluso cuando se usa 0 como retraso, todos los navegadores tienen un límite mínimo de retraso de tiempo de espera (especificado en 4 ms en la especificación HTML5).
  • jQuery $.postLa devolución de llamada se puede llamar algún día en el futuro, cuando (y si) la solicitud de Ajax se ha completado con éxito.
  • Node.js's fs.readFile puede ser llamado algún día en el futuro, cuando el archivo se ha leído correctamente o se ha producido un error.

En todos los casos, tenemos una devolución de llamada que se puede ejecutar algún día en el futuro. Este "alguna vez en el futuro" es a lo que nos referimos como flujo asincrónico.

La ejecución asincrónica se saca del flujo síncrono. Es decir, el código asíncrono Nunca ejecutar mientras se está ejecutando la pila de código síncrono. Este es el significado de que JavaScript sea de subproceso único.

Más específicamente, cuando el motor JS está inactivo, no ejecuta una pila de (a) código síncrono, buscará eventos que pueden haber desencadenado devoluciones de llamada asíncronas (por ejemplo, expiración expirada, respuesta de red recibida) y los ejecutará uno tras otro. Esto es considerado como Evento Loop.

Es decir, el código asíncrono resaltado en las formas rojas dibujadas a mano se puede ejecutar solo después de que se haya ejecutado todo el código síncrono restante en sus respectivos bloques de código:

async code highlighted

En resumen, las funciones de devolución de llamada se crean de forma síncrona pero se ejecutan de forma asíncrona. 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.

Es simple, realmente. La lógica que depende de la ejecución de la función asincrónica debe iniciarse / invocarse desde el interior de esta función asíncrona. Por ejemplo, mover el alerts y console.logs también dentro de la función de devolución de llamada generará 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 ha llamado a la función asíncrona. Vamos a abordar 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 asincrónica genérica, el mismo ejemplo se aplica a Ajax, readFile, onload y cualquier otro flujo asincrónico.

Este ejemplo claramente adolece del 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. Primero, 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 que pasa 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);
}

En la mayoría de los casos 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á a partir 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 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 asíncronas se ejecutan mucho después de que el código síncrono ya haya terminado de ejecutarse.

En lugar de returnAl obtener un valor de una devolución de llamada asincrónica, tendrá que utilizar el patrón de devolución de llamada, o ... Promesas.

Promesas

Aunque hay formas de mantener el infierno de devolución de llamada a raya con JS vainilla, las promesas están creciendo en popularidad y actualmente están siendo estandarizadas en ES6 (ver Promesa - MDN)

Las promesas (a.k.a. futuros) proporcionan una lectura más lineal, y por lo tanto agradable, del código asíncrono, pero explicar su funcionalidad completa está fuera del alcance de esta pregunta. En cambio, dejaré estos recursos excelentes para el interesado:


Más material de lectura sobre asincronicidad de JavaScript


Nota:Marqué 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.


441
2018-01-20 23:42



La respuesta de Fabricio es perfecta; 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 cierta información de un colega. Lo llamé; así es como fue la conversación:

Yo: Hola Bob, necesito saber cómo food el barLa semana pasada. Jim quiere un informe y tú eres el único que conoce los detalles al respecto.

Chelín: Claro, pero me llevará alrededor de 30 minutos?

Yo: Eso es genial Bob. ¡Devuélveme un anillo 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 me devolvió la llamada 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 seguido así;

Yo: Hola Bob, necesito saber cómo food el barLa semana pasada. Jim quiere un informe y tú eres el único que conoce los detalles al respecto.

Chelín: Claro, pero me llevará alrededor de 30 minutos?

Yo: Eso es genial Bob. Esperaré.

Y me senté allí y esperé. Y esperó. Y esperó. Por 40 minutos No haciendo nada más que esperar. Eventualmente, Bob me dio la información, colgamos y completé mi informe. Pero perdí 40 minutos de productividad.


Esto es un comportamiento asíncrono vs. sincrónico

Esto es exactamente lo que está sucediendo en todos los ejemplos en nuestra pregunta. Cargar una imagen, cargar un archivo en el disco y solicitar una página a través de AJAX son operaciones lentas (en el contexto de la informática moderna).

Más bien que esperando para que estas operaciones lentas se completen, JavaScript le permite registrar una función de devolución de llamada que se ejecutará cuando la operación lenta se haya completado. Mientras tanto, sin embargo, JavaScript continuará ejecutando otro código. El hecho de que JavaScript se ejecuta otro código mientras espera que se complete la operación lenta, el comportamientoasincrónico. Si JavaScript hubiera esperado para completar la operación antes de ejecutar cualquier otro código, esto habría sido sincrónico comportamiento.

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 estamos pidiendo a JavaScript que cargue lolcat.png, el cual es un sloooow operación. La función de devolución de llamada se ejecutará una vez que se haya realizado esta operación lenta, pero mientras tanto, JavaScript continuará procesando las siguientes líneas de código; es decir alert(outerScopeVar).

Es por eso que vemos la alerta que muestra undefined; ya que el alert() se procesa inmediatamente, en lugar de después de cargar la imagen.

Para arreglar nuestro código, todo lo que tenemos que hacer es mover el alert(outerScopeVar) código dentro la función de devolución de llamada. Como consecuencia de esto, ya no necesitamos el 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 ver que una devolución de llamada se especifica como una función, porque esa es la única * forma en JavaScript para definir algún código, pero no ejecutarlo hasta más tarde.

Por lo tanto, en todos nuestros ejemplos, el function() { /* Do something */ } es la devolución de llamada; arreglar todas los ejemplos, ¡todo lo que tenemos que hacer es mover el código que necesita la respuesta de la operación hacia allí!

* Técnicamente puedes usar eval() también, pero eval() es malvado para este propósito


¿Cómo puedo mantener a mi interlocutor esperando?

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 sucede de inmediato; antes de onload la función de devolución de llamada ha actualizado la variable. Esto lleva a getWidthOfImage() regresando undefinedy undefined siendo alertado

Para solucionar esto, necesitamos permitir que la función llame getWidthOfImage() para registrar 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)


118
2017-12-08 16:48



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 que setTimeout se llama y se actualiza outerScopeVar.

Las dos formas principales de resolverlo están usando devoluciones de llamada y promesas:

Devolución de llamada

El cambio aquí es que getMessage acepta una 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);
});

Promesas

Las promesas ofrecen una alternativa que es más flexible que las devoluciones de llamada porque se pueden combinar de forma natural para coordinar múltiples operaciones asincrónicas. UN Promesas / A + la implementación estándar se proporciona de forma nativa en node.js (0.12+) y muchos navegadores actuales, pero también se implementa en bibliotecas como Azulejo y Q.

function getMessage() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve('Hello asynchronous world!');
    }, 0);
  });
}

getMessage().then(function(message) {
  console.log(message);  
});

jQuery Deferreds

jQuery proporciona una funcionalidad similar a las promesas con sus Deferreds.

function getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

getMessage().done(function(message) {
  console.log(message);  
});

asincronizar / esperar

Si su entorno JavaScript incluye soporte para async y await (como Node.js 7.6+), entonces puedes usar promesas sincrónicamente dentro 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();

55
2018-02-26 03:59



Para decir lo obvio, la copa representa outerScopeVar.

Las funciones asincrónicas son como ...

asynchronous call for coffee


44
2017-10-27 06:35



Las otras respuestas son excelentes y solo quiero dar una respuesta directa a esto. Solo limitando a las llamadas asíncronas jQuery

Todas las llamadas ajax (incluido el $.get o $.post o $.ajax) son asincrónicos.

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 disparadores y la llamada asíncrona en la línea 2 (es decir, la solicitud posterior) y continúa su ejecución desde la línea 3, sin esperar a que la solicitud posterior 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 ejecutas esto, obtendrías una alerta en la línea 3. Ahora espera un tiempo hasta que estés 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 próxima alerta imprimirá el valor esperado, porque lo esperó.

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 asíncronas se mueve dentro del bloque asíncrono o esperando en las llamadas asincrónicas.


11



En todos estos escenarios outerScopeVar se modifica o se le asigna un valor asincrónicamente o sucediendo en un momento posterior (esperando o escuchando que ocurra algún evento), para lo cual la ejecución actual no esperará. Así que todos estos casos actuales resultados de flujo de ejecución en outerScopeVar = undefined

Analicemos cada ejemplo (marqué la porción que se llama asincrónicamente o se retrasa para que ocurran algunos eventos):

1.

enter image description here

Aquí registramos un eventlistner que se ejecutará en ese evento en particular. Aquí se carga 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, funtion img.onload espere a que cargue la imagen referida, de manera asincrónica. Esto sucederá en todos los ejemplos siguientes: el evento puede diferir.

2.

2

Aquí el evento de tiempo de espera juega el rol, que invocará al controlador después del tiempo especificado. Aquí está 0, pero aún así registra un evento asíncrono que se agregará a la última posición de la Event Queue para la ejecución, que hace el retraso garantizado.

3.

enter image description here Esta vez ajax callback.

4.

enter image description here

El nodo se puede considerar como un rey de la codificación asíncrona. 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.

enter image description here

La promesa obvia (algo se hará en el futuro) es asincrónico. ver ¿Cuáles son las diferencias entre Deferred, Promise y Future en JavaScript?

https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript


8