¿Por qué no se modifica mi variable después de modificarla dentro de una función? - Referencia de código asíncrono


669

Dados los siguientes ejemplos, ¿por qué outerScopeVarno 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é sale undefineden todos estos ejemplos? No quiero soluciones, quiero saber por qué sucede esto.


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



@Dukeling, gracias, estoy bastante seguro de que había comentado con ese enlace, pero aparentemente faltan algunos comentarios. Además, con respecto a su edición: creo que tener "canónico" y "asincronía" en el título ayuda al buscar esta pregunta para marcar otra pregunta como un engaño. Y, por supuesto, también ayuda a encontrar esta pregunta de Google cuando busca explicaciones de asincronía.
Fabrício Matté

3
Poniendo un poco más de pensamiento, "el tema de la asincronía canónica" es un poco pesado en el título, "la referencia de código asincrónico" es más simple y más objetivo. También creo que la mayoría de la gente busca "asincrónica" en lugar de "asincrónica".
Fabrício Matté

1
Algunas personas inicializan su variable antes de la llamada a la función. ¿Qué tal cambiar el título que de alguna manera representa eso también? Como "¿Por qué no se altera mi variable después de modificarla dentro de una función?" ?
Felix Kling

En todos los ejemplos de código que ha mencionado anteriormente, "alert (externalScopeVar);" se ejecuta AHORA, mientras que la asignación de valor a "outsideScopeVar" ocurre MÁS TARDE (asincrónicamente).
refactorizado

Respuestas:


542

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, outerScopeVarse 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.onloadse puede llamar en algún momento en el futuro , cuando (y si) la imagen se haya cargado correctamente.
  • setTimeoutse 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 0como retraso, todos los navegadores tienen un límite de tiempo de espera mínimo (especificado en 4 ms en la especificación HTML5).
  • $.postLa 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.readFileSe 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:

código asíncrono resaltado

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 alertsys console.logtambié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 setTimeoutcon un retraso aleatorio como una función asíncrona genérico, el mismo ejemplo se aplica a Ajax, readFile, onloady 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 outerScopeVarque 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 helloCatAsyncimplementació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 returnun 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 returnun 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 !


1
En su último ejemplo, ¿hay alguna razón específica por la que use funciones anónimas o funcionaría de la misma manera usando funciones con nombre?
JDelage

1
Los ejemplos de código son un poco extraños ya que declara la función después de llamarla. Funciona debido a la elevación, por supuesto, pero ¿fue intencional?
Bergi

2
¿Es un punto muerto? felix kling está apuntando a su respuesta y usted está apuntando a la respuesta de felix
Mahi

1
Debe comprender que el código del círculo rojo es solo asíncrono porque lo ejecutan las funciones NATIVE async javascript. Esta es una característica de su motor de JavaScript, ya sea Node.js o un navegador. Es asíncrono porque se pasa como "devolución de llamada" a una función que es esencialmente un cuadro negro (implementado en C, etc.). Para el desafortunado desarrollador, son asíncronos ... solo porque sí. Si desea escribir su propia función asíncrona, debe piratearla enviándola a SetTimeout (myfunc, 0). ¿Deberías hacer eso? Otro debate ... probablemente no.
Sean Anderson el

@Fabricio Busqué la especificación que define la "> = abrazadera de 4 ms", pero no pude encontrarla. Encontré alguna mención de un mecanismo similar (para sujetar llamadas anidadas) en MDN - developer.mozilla.org/en-US/docs / Web / API / ... : ¿alguien tiene un enlace a la parte correcta de la especificación HTML?
Sebi

147

La respuesta de Fabricio 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 asincronía .


Una analogía ...

Ayer, el trabajo que estaba haciendo requería información de un colega. Lo llamé; así es como fue la conversación:

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

Bob : Claro, ¿pero me llevará unos 30 minutos?

Yo : Eso es genial Bob. ¡Llámame 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é, y luego recibí 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í en su lugar;

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

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. No haciendo 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 asincrónico versus síncrono

Esto es exactamente lo que está sucediendo en todos los ejemplos en nuestra pregunta. Cargar una imagen, cargar un archivo desde el disco y solicitar una página a través de AJAX son 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 continuará 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 asíncrono . 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 que cargue JavaScript lolcat.png, que es una operación lenta . La función de devolución de llamada se ejecutará una vez que esta operación lenta haya terminado, 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 de inmediato, 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 outerScopeVarvariable declarada como una 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, el function() { /* Do something */ }es la devolución de llamada; Para solucionar todos los ejemplos, todo lo que tenemos que hacer es mover el código que necesita la respuesta de la operación allí.

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


¿Cómo hago esperar a la persona que llama?

Es posible que actualmente tenga un 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 outerScopeVarsucede inmediatamente; antes de que la onloadfunción de devolución de llamada haya actualizado la variable. Esto lleva a getWidthOfImage()regresar undefinedy undefinedser alertado.

Para solucionar esto, debemos 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).


Pero, ¿cómo es útil alertar o enviar a la consola si desea utilizar los resultados en un cálculo diferente o almacenarlo en una variable de objeto?
Ken Ingram

68

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 / wait.

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());

undefinedse registra en este caso porque getMessageregresa antes de que setTimeoutse llame la devolución de llamada y se actualiza outerScopeVar.

Las dos formas principales de resolverlo son mediante devoluciones de llamada y promesas :

Devoluciones de llamada

El cambio aquí es que getMessageacepta un callbackparámetro que se llamará para entregar 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 proporcionan una alternativa que es más flexible que las devoluciones de llamada porque pueden combinarse naturalmente 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);  
});

jQuery diferidos

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);  
});

asíncrono / esperar

Si su entorno JavaScript incluye soporte para asyncy await(como Node.js 7.6+), entonces puede usar promesas sincrónicamente dentro de las asyncfunciones:

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();

Su muestra de Promesas es básicamente lo que he estado buscando durante las últimas horas. Su ejemplo es hermoso y explica las promesas al mismo tiempo. Por qué esto no está en ningún otro lugar es alucinante.
Vincent P

Todo esto está bien, pero ¿qué pasa si necesita llamar a getMessage () con parámetros? ¿Cómo escribirías lo anterior en ese escenario?
Chiwda

2
@Chiwda Usted acaba de poner el parámetro de devolución de llamada pasada: function getMessage(param1, param2, callback) {...}.
JohnnyHK

Estoy probando tu async/awaitmuestra, pero me encuentro con problemas. En lugar de crear una instancia de a new Promise, estoy haciendo una .Get()llamada y, por lo tanto, no tengo acceso a ningún resolve()método. Por lo tanto, mi getMessage()está devolviendo la Promesa y no el resultado. ¿Podría editar un poco su respuesta para mostrar una sintaxis funcional para esto?
InteXX

@InteXX No estoy seguro de lo que quieres decir con hacer una .Get()llamada. Probablemente sea mejor publicar una nueva pregunta.
JohnnyHK

52

Para decir lo obvio, la copa representa outerScopeVar.

Las funciones asincrónicas son como ...

llamada asincrónica para café


13
Mientras que tratar de hacer que una función asincrónica actúe sincrónicamente sería tratar de tomar el café en 1 segundo y verterlo en su regazo en 1 minuto.
Teepeemm

Si dijera lo obvio, no creo que se hubiera hecho la pregunta, ¿no?
broccoli2000

2
@ broccoli2000 Con eso no quise decir que la pregunta era obvia, sino que es obvio lo que representa la copa en el dibujo :)
Johannes Fahrenkrug

13

Las otras respuestas son excelentes y solo quiero proporcionar una respuesta directa a esto. Solo limitando a llamadas asincrónicas jQuery

Todas las llamadas ajax (incluidas las $.getor $.posto $.ajax) son asíncronas.

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 los 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 outerScopeVarsolo 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 momento hasta que esté seguro de que la solicitud de publicación ha devuelto algún valor. Luego, cuando hace clic en Aceptar, en el cuadro de alerta, la siguiente 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 asincrónicas, se mueve dentro del bloque asincrónico, o al esperar las llamadas asincrónicas.


or by waiting on the asynchronous calls¿Cómo se hace eso?
InteXX

@InteXX Mediante el uso de un método de devolución de llamada
Teja

¿Tienes un ejemplo de sintaxis rápida?
InteXX

10

En todos estos escenarios outerScopeVarse modifica o se le asigna un valor de forma asincrónica o sucede en un momento posterior (esperando o escuchando que ocurra algún evento), para lo cual la ejecución actual no esperará . Por lo tanto, todos estos casos el flujo de ejecución actual da como resultadoouterScopeVar = undefined

Analicemos cada ejemplo (marqué la parte que se llama de forma asíncrona o retrasada para que ocurran algunos eventos):

1)

ingrese la descripción de la imagen aquí

Aquí registramos una lista de eventos 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, la función img.onloadespera a que se cargue la imagen referida de forma asincrónica. Esto sucederá todo el siguiente ejemplo: el evento puede diferir.

2)

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 registra un evento asincrónico que se agregará a la última posición de Event Queueejecución, lo que hace que el retraso esté garantizado.

3)

ingrese la descripción de la imagen aquí Esta vez ajax callback.

4)

ingrese la descripción de la imagen aquí

El nodo se puede considerar como un 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)

ingrese la descripción de la imagen aquí

La promesa obvia (se hará algo en el futuro) es asíncrona. ver ¿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