¿Cómo devuelvo la respuesta de una llamada asincrónica?


5512

Tengo una función fooque hace una solicitud de Ajax. ¿Cómo puedo devolver la respuesta foo?

Intenté devolver el valor de la successdevolución de llamada, así como asignar la respuesta a una variable local dentro de la función y devolver esa, pero ninguna de esas formas realmente devuelve la respuesta.

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
            // return response; // <- I tried that one as well
        }
    });

    return result;
}

var result = foo(); // It always ends up being `undefined`.

Respuestas:


5704

→ Para obtener una explicación más general del comportamiento asíncrono con diferentes ejemplos, consulte ¿Por qué no se altera mi variable después de modificarla dentro de una función? - Referencia de código asíncrono

→ Si ya comprende el problema, pase a las posibles soluciones a continuación.

El problema

La A en Ajax significa asíncrono . Eso significa que enviar la solicitud (o más bien recibir la respuesta) se elimina del flujo de ejecución normal. En su ejemplo, $.ajaxdevuelve inmediatamente y la siguiente instrucción, return result;se ejecuta antes de que successincluso se llamara a la función que pasó como devolución de llamada.

Aquí hay una analogía que, con suerte, hace más clara la diferencia entre flujo síncrono y asíncrono:

Sincrónico

Imagine que hace una llamada telefónica a un amigo y le pide que busque algo por usted. Aunque puede llevar un tiempo, espera en el teléfono y mira fijamente al espacio, hasta que su amigo le dé la respuesta que necesitaba.

Lo mismo sucede cuando realiza una llamada de función que contiene el código "normal":

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();

Aunque findItempuede llevar mucho tiempo ejecutarlo, cualquier código posterior var item = findItem();tiene que esperar hasta que la función devuelva el resultado.

Asincrónico

Llamas a tu amigo nuevamente por la misma razón. Pero esta vez le dices que tienes prisa y que debería llamarte de nuevo a tu teléfono móvil. Cuelgas, sales de casa y haces lo que planeaste hacer. Una vez que tu amigo te devuelve la llamada, estás lidiando con la información que te dio.

Eso es exactamente lo que sucede cuando haces una solicitud de Ajax.

findItem(function(item) {
    // Do something with item
});
doSomethingElse();

En lugar de esperar la respuesta, la ejecución continúa inmediatamente y la declaración después de que se ejecuta la llamada Ajax. Para obtener la respuesta, finalmente, se proporciona una función que se llama una vez la respuesta fue recibida, una devolución de llamada (aviso de algo? Posterior llamada ?). Cualquier declaración posterior a esa llamada se ejecuta antes de que se llame la devolución de llamada.


Solución (es)

¡Abrace la naturaleza asincrónica de JavaScript!Si bien ciertas operaciones asincrónicas proporcionan contrapartidas sincrónicas (también lo hace "Ajax"), generalmente se desaconseja usarlas, especialmente en un contexto de navegador.

¿Por qué es malo lo preguntas?

JavaScript se ejecuta en el hilo de la interfaz de usuario del navegador y cualquier proceso de larga duración bloqueará la interfaz de usuario, por lo que no responde. Además, hay un límite superior en el tiempo de ejecución de JavaScript y el navegador le preguntará al usuario si debe continuar la ejecución o no.

Todo esto es una experiencia de usuario realmente mala. El usuario no podrá saber si todo funciona bien o no. Además, el efecto será peor para los usuarios con una conexión lenta.

A continuación, veremos tres soluciones diferentes que se están construyendo una encima de la otra:

  • Promete conasync/await (ES2017 +, disponible en navegadores antiguos si usa un transpilador o regenerador)
  • Callbacks (popular en nodo)
  • Promesas conthen() (ES2015 +, disponible en navegadores antiguos si usa una de las muchas bibliotecas de promesas)

Los tres están disponibles en los navegadores actuales y en el nodo 7+.


ES2017 +: Promesas con async/await

La versión ECMAScript lanzada en 2017 introdujo soporte de nivel de sintaxis para funciones asincrónicas. Con la ayuda de asyncy await, puede escribir de forma asíncrona en un "estilo sincrónico". El código sigue siendo asíncrono, pero es más fácil de leer / comprender.

async/awaitse basa en promesas: una asyncfunción siempre devuelve una promesa. await"desenvuelve" una promesa y resulta en el valor con el que se resolvió la promesa o arroja un error si la promesa fue rechazada.

Importante: Solo puede usar awaitdentro de una asyncfunción. En este momento, el nivel superior awaitaún no es compatible, por lo que es posible que tenga que hacer una IIFE asincrónica ( Expresión de función invocada inmediatamente ) para iniciar unasync contexto.

Puede leer más sobre asyncy awaiten MDN.

Aquí hay un ejemplo que se basa en el retraso anterior:

// Using 'superagent' which will return a promise.
var superagent = require('superagent')

// This is isn't declared as `async` because it already returns a promise
function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}


async function getAllBooks() {
  try {
    // GET a list of book IDs of the current user
    var bookIDs = await superagent.get('/user/books');
    // wait for 3 seconds (just for the sake of this example)
    await delay();
    // GET information about each book
    return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
  } catch(error) {
    // If any of the awaited promises was rejected, this catch block
    // would catch the rejection reason
    return null;
  }
}

// Start an IIFE to use `await` at the top level
(async function(){
  let books = await getAllBooks();
  console.log(books);
})();

Soporte de versiones actuales de navegador y nodoasync/await . También puede admitir entornos más antiguos transformando su código a ES5 con la ayuda de regenerator (o herramientas que usan regenerator, como Babel ).


Dejar que las funciones acepten devoluciones de llamada

Una devolución de llamada es simplemente una función pasada a otra función. Esa otra función puede llamar a la función pasada siempre que esté lista. En el contexto de un proceso asincrónico, la devolución de llamada se llamará siempre que se realice el proceso asincrónico. Por lo general, el resultado se pasa a la devolución de llamada.

En el ejemplo de la pregunta, puede hacer que fooacepte una devolución de llamada y usarla como successdevolución de llamada. Así que esto

var result = foo();
// Code that depends on 'result'

se convierte

foo(function(result) {
    // Code that depends on 'result'
});

Aquí definimos la función "en línea" pero puede pasar cualquier referencia de función:

function myCallback(result) {
    // Code that depends on 'result'
}

foo(myCallback);

foo en sí se define de la siguiente manera:

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}

callbackse referirá a la función a la que pasamos foocuando la llamamos y simplemente la pasamos a success. Es decir, una vez que la solicitud de Ajax sea exitosa, $.ajaxllamará callbacky pasará la respuesta a la devolución de llamada (que se puede consultar conresult , ya que así es como definimos la devolución de llamada).

También puede procesar la respuesta antes de pasarla a la devolución de llamada:

function foo(callback) {
    $.ajax({
        // ...
        success: function(response) {
            // For example, filter the response
            callback(filtered_response);
        }
    });
}

Es más fácil escribir código usando devoluciones de llamada de lo que parece. Después de todo, JavaScript en el navegador depende en gran medida de los eventos (eventos DOM). Recibir la respuesta de Ajax no es más que un evento.
Pueden surgir dificultades cuando tiene que trabajar con código de terceros, pero la mayoría de los problemas se pueden resolver con solo pensar en el flujo de la aplicación.


ES2015 +: Promesas con then ()

La promesa de la API es una nueva característica de ECMAScript 6 (ES2015), pero tiene buen soporte de los navegadores ya. También hay muchas bibliotecas que implementan la API Promises estándar y proporcionan métodos adicionales para facilitar el uso y la composición de funciones asincrónicas (por ejemplo, bluebird ).

Las promesas son contenedores para valores futuros . Cuando la promesa recibe el valor (se resuelve ) o cuando se cancela ( rechaza ), notifica a todos sus "oyentes" que desean acceder a este valor.

La ventaja sobre las devoluciones de llamada simples es que le permiten desacoplar su código y son más fáciles de componer.

Aquí hay un ejemplo simple de uso de una promesa:

function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}

delay()
  .then(function(v) { // `delay` returns a promise
    console.log(v); // Log the value once it is resolved
  })
  .catch(function(v) {
    // Or do something else if it is rejected 
    // (it would not happen in this example, since `reject` is not called).
  });

Aplicado a nuestra llamada Ajax, podríamos usar promesas como esta:

function ajax(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open('GET', url);
    xhr.send();
  });
}

ajax("/echo/json")
  .then(function(result) {
    // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });

Describir todas las ventajas que promete ofrecer está más allá del alcance de esta respuesta, pero si escribe un código nuevo, debería considerarlas seriamente. Proporcionan una gran abstracción y separación de su código.

Más información sobre promesas: HTML5 rocks - JavaScript Promises

Nota al margen: los objetos diferidos de jQuery

Los objetos diferidos son la implementación personalizada de promesas de jQuery (antes de que la API Promise se estandarizara). Se comportan casi como promesas, pero exponen una API ligeramente diferente.

Todos los métodos Ajax de jQuery ya devuelven un "objeto diferido" (en realidad una promesa de un objeto diferido) que puede devolver de su función:

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

Nota al margen: Promesas Gotchas

Tenga en cuenta que las promesas y los objetos diferidos son solo contenedores para un valor futuro, no son el valor en sí. Por ejemplo, suponga que tiene lo siguiente:

function checkPassword() {
    return $.ajax({
        url: '/password',
        data: {
            username: $('#username').val(),
            password: $('#password').val()
        },
        type: 'POST',
        dataType: 'json'
    });
}

if (checkPassword()) {
    // Tell the user they're logged in
}

Este código no comprende los problemas de asincronía anteriores. Específicamente, $.ajax()no congela el código mientras verifica la página '/ contraseña' en su servidor: envía una solicitud al servidor y mientras espera, inmediatamente devuelve un objeto jQuery Ajax diferido, no la respuesta del servidor. Eso significa que la ifdeclaración siempre obtendrá este objeto diferido, lo tratará como truey procederá como si el usuario hubiera iniciado sesión. No es bueno.

Pero la solución es fácil:

checkPassword()
.done(function(r) {
    if (r) {
        // Tell the user they're logged in
    } else {
        // Tell the user their password was bad
    }
})
.fail(function(x) {
    // Tell the user something bad happened
});

No recomendado: llamadas síncronas "Ajax"

Como mencioné, algunas (!) Operaciones asincrónicas tienen contrapartidas sincrónicas. No recomiendo su uso, pero para completar, así es como realizaría una llamada síncrona:

Sin jQuery

Si usa directamente un XMLHTTPRequestobjeto, pase falsecomo tercer argumento a .open.

jQuery

Si usa jQuery , puede establecer la asyncopción en false. Tenga en cuenta que esta opción está en desuso desde jQuery 1.8. A continuación, puede seguir utilizando una successdevolución de llamada o acceder a la responseTextpropiedad del objeto jqXHR :

function foo() {
    var jqXHR = $.ajax({
        //...
        async: false
    });
    return jqXHR.responseText;
}

Si usa cualquier otro método jQuery Ajax, como $.get, $.getJSONetc., debe cambiarlo a $.ajax(ya que solo puede pasar parámetros de configuración a $.ajax).

¡Aviso! No es posible realizar una solicitud JSONP sincrónica . JSONP, por su propia naturaleza, siempre es asíncrono (una razón más para ni siquiera considerar esta opción).


74
@Pommy: si desea usar jQuery, debe incluirlo. Consulte docs.jquery.com/Tutorials:Getting_Started_with_jQuery .
Felix Kling

11
En la Solución 1, sub jQuery, no pude entender esta línea: If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.(Sí, me doy cuenta de que mi nick es un poco irónico en este caso)
cssyphus

32
@gibberish: Mmmh, no sé cómo se puede aclarar. ¿Ves cómo foose llama y se le pasa una función ( foo(function(result) {....});)? resultse utiliza dentro de esta función y es la respuesta de la solicitud de Ajax. Para referirse a esta función, se llama al primer parámetro de foo callbacky se le asigna en successlugar de una función anónima. Entonces, $.ajaxllamará callbackcuando la solicitud haya sido exitosa. Traté de explicarlo un poco más.
Felix Kling

43
El chat para esta pregunta está muerto, así que no estoy seguro de dónde proponer los cambios descritos, pero propongo: 1) Cambiar la parte sincrónica a una simple discusión de por qué es malo sin un ejemplo de código de cómo hacerlo. 2) Elimine / combine los ejemplos de devolución de llamada para mostrar solo el enfoque diferido más flexible, que creo que también puede ser un poco más fácil de seguir para quienes aprenden Javascript.
Chris Moschini

14
@Jessi: Creo que entendiste mal esa parte de la respuesta. No puede usar $.getJSONsi desea que la solicitud de Ajax sea sincrónica. Sin embargo, no debe desear que la solicitud sea síncrona, por lo que no se aplica. Debería usar devoluciones de llamada o promesas para manejar la respuesta, como se explica anteriormente en la respuesta.
Felix Kling

1071

Si estás no usando jQuery en el código, esta respuesta es para usted

Su código debe ser algo similar a esto:

function foo() {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
    return httpRequest.responseText;
}

var result = foo(); // always ends up being 'undefined'

Felix Kling hizo un buen trabajo escribiendo una respuesta para las personas que usan jQuery para AJAX, he decidido proporcionar una alternativa para las personas que no lo son.

( Nota, para aquellos que usan la nueva fetchAPI, Angular o promesas, he agregado otra respuesta a continuación )


A lo que te enfrentas

Este es un breve resumen de "Explicación del problema" de la otra respuesta, si no está seguro después de leer esto, léalo.

La A en AJAX significa asíncrono . Eso significa que enviar la solicitud (o más bien recibir la respuesta) se elimina del flujo de ejecución normal. En su ejemplo, .senddevuelve inmediatamente y la siguiente instrucción, return result;se ejecuta antes de que successincluso se llamara a la función que pasó como devolución de llamada.

Esto significa que cuando regresa, el oyente que ha definido aún no se ejecutó, lo que significa que el valor que está devolviendo no se ha definido.

Aquí hay una analogía simple

function getFive(){ 
    var a;
    setTimeout(function(){
         a=5;
    },10);
    return a;
}

(Violín)

El valor de areturn es undefinedporque la a=5parte aún no se ha ejecutado. AJAX actúa así, está devolviendo el valor antes de que el servidor tenga la oportunidad de decirle a su navegador cuál es ese valor.

Una posible solución a este problema es codificar de forma reactiva , diciéndole a su programa qué hacer cuando se complete el cálculo.

function onComplete(a){ // When the code completes, do this
    alert(a);
}

function getFive(whenDone){ 
    var a;
    setTimeout(function(){
         a=5;
         whenDone(a);
    },10);
}

Esto se llama CPS . Básicamente, estamos pasando getFiveuna acción a realizar cuando se completa, le estamos diciendo a nuestro código cómo reaccionar cuando se completa un evento (como nuestra llamada AJAX, o en este caso el tiempo de espera).

El uso sería:

getFive(onComplete);

Lo que debería alertar "5" a la pantalla. (Violín) .

Soluciones posibles

Básicamente, hay dos formas de resolver esto:

  1. Haga que la llamada AJAX sea sincrónica (llamémosla SJAX).
  2. Reestructura tu código para que funcione correctamente con devoluciones de llamada.

1. AJAX sincrónico: ¡no lo hagas!

En cuanto a AJAX síncrono, ¡no lo hagas! La respuesta de Felix plantea algunos argumentos convincentes sobre por qué es una mala idea. Para resumir, congelará el navegador del usuario hasta que el servidor devuelva la respuesta y cree una experiencia de usuario muy mala. Aquí hay otro breve resumen tomado de MDN sobre por qué:

XMLHttpRequest admite comunicaciones sincrónicas y asincrónicas. Sin embargo, en general, las solicitudes asíncronas deben preferirse a las solicitudes síncronas por razones de rendimiento.

En resumen, las solicitudes sincrónicas bloquean la ejecución del código ... ... esto puede causar serios problemas ...

Si tiene que hacerlo, puede pasar una bandera: así es como:

var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false);  // `false` makes the request synchronous
request.send(null);

if (request.status === 200) {// That's HTTP for 'ok'
  console.log(request.responseText);
}

2. Código de reestructuración

Deje que su función acepte una devolución de llamada. En el ejemplo, foose puede hacer que el código acepte una devolución de llamada. Le diremos a nuestro código cómo reaccionar cuando se foocomplete.

Entonces:

var result = foo();
// code that depends on `result` goes here

Se convierte en:

foo(function(result) {
    // code that depends on `result`
});

Aquí pasamos una función anónima, pero podríamos pasar fácilmente una referencia a una función existente, haciendo que se vea así:

function myHandler(result) {
    // code that depends on `result`
}
foo(myHandler);

Para obtener más detalles sobre cómo se hace este tipo de diseño de devolución de llamada, consulte la respuesta de Felix.

Ahora, definamos foo para actuar en consecuencia

function foo(callback) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.onload = function(){ // when the request is loaded
       callback(httpRequest.responseText);// we're calling our method
    };
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
}

(violín)

Ahora hemos hecho que nuestra función foo acepte una acción para ejecutarse cuando el AJAX se complete con éxito, podemos extender esto más allá verificando si el estado de respuesta no es 200 y actuando en consecuencia (cree un controlador de fallas y tal). Resolviendo efectivamente nuestro problema.

Si aún le cuesta entender esto, lea la guía de inicio de AJAX en MDN.


20
"las solicitudes sincrónicas bloquean la ejecución de código y pueden perder memoria y eventos" ¿cómo puede una solicitud sincrónica perder memoria?
Matthew G

10
@MatthewG He agregado una recompensa en esta pregunta , veré qué puedo pescar. Estoy eliminando la cita de la respuesta mientras tanto.
Benjamin Gruenbaum

17
Solo como referencia, XHR 2 nos permite usar el onloadcontrolador, que solo se activa cuando readyStateestá 4. Por supuesto, no es compatible con IE8. (IIRC, puede necesitar confirmación.)
Florian Margaine

99
Su explicación de cómo pasar una función anónima como devolución de llamada es válida pero engañosa. El ejemplo var bar = foo (); está pidiendo que se defina una variable, mientras que su foo sugerido (functim () {}); no define barra
Robbie Averill

396

XMLHttpRequest 2 (en primer lugar, lea las respuestas de Benjamin Gruenbaum y Felix Kling )

Si no usa jQuery y desea un buen XMLHttpRequest 2 que funcione en los navegadores modernos y también en los navegadores móviles, sugiero que lo use de esta manera:

function ajax(a, b, c){ // URL, callback, just a placeholder
  c = new XMLHttpRequest;
  c.open('GET', a);
  c.onload = b;
  c.send()
}

Como puedes ver:

  1. Es más corto que todas las demás funciones enumeradas.
  2. La devolución de llamada se establece directamente (por lo que no hay cierres innecesarios adicionales).
  3. Utiliza la nueva carga (para que no tenga que verificar el estado listo &&)
  4. Hay otras situaciones que no recuerdo que hacen que XMLHttpRequest 1 sea molesto.

Hay dos formas de obtener la respuesta de esta llamada Ajax (tres usando el nombre var XMLHttpRequest):

Lo más simple:

this.response

O si por alguna razón le bind()devuelven la llamada a una clase:

e.target.response

Ejemplo:

function callback(e){
  console.log(this.response);
}
ajax('URL', callback);

O (lo anterior es mejor, las funciones anónimas siempre son un problema):

ajax('URL', function(e){console.log(this.response)});

Nada mas facil.

Ahora, algunas personas probablemente dirán que es mejor usar onreadystatechange o incluso el nombre de variable XMLHttpRequest. Eso está mal.

Consulte las funciones avanzadas de XMLHttpRequest

Soporta todos los * navegadores modernos. Y puedo confirmar que estoy usando este enfoque ya que XMLHttpRequest 2 existe. Nunca tuve ningún tipo de problema en todos los navegadores que uso.

onreadystatechange solo es útil si desea obtener los encabezados en el estado 2.

El uso del XMLHttpRequestnombre de la variable es otro gran error, ya que necesita ejecutar la devolución de llamada dentro de los cierres onload / oreadystatechange; de ​​lo contrario, lo perdió.


Ahora, si desea algo más complejo usando post y FormData, puede extender fácilmente esta función:

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.send(d||null)
}

Una vez más ... es una función muy corta, pero obtiene y publica.

Ejemplos de uso:

x(url, callback); // By default it's get so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set post data

O pasar un elemento de forma completa ( document.getElementsByTagName('form')[0]):

var fd = new FormData(form);
x(url, callback, 'post', fd);

O establezca algunos valores personalizados:

var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);

Como puede ver, no implementé la sincronización ... es algo malo.

Habiendo dicho eso ... ¿por qué no hacerlo de la manera fácil?


Como se mencionó en el comentario, el uso de error && synchronous rompe por completo el punto de la respuesta. ¿Cuál es una buena forma de usar Ajax de la manera adecuada?

Controlador de errores

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.onerror = error;
  c.send(d||null)
}

function error(e){
  console.log('--Error--', this.type);
  console.log('this: ', this);
  console.log('Event: ', e)
}
function displayAjax(e){
  console.log(e, this);
}
x('WRONGURL', displayAjax);

En la secuencia de comandos anterior, tiene un controlador de errores que está estáticamente definido para que no comprometa la función. El controlador de errores también se puede utilizar para otras funciones.

Pero para sacar realmente un error, la única forma es escribir una URL incorrecta, en cuyo caso cada navegador arroja un error.

Los controladores de errores pueden ser útiles si configura encabezados personalizados, establece el tipo de respuesta en búfer de matriz de blobs o lo que sea ...

Incluso si pasa 'POSTAPAPAP' como método, no arrojará un error.

Incluso si pasa 'fdggdgilfdghfldj' como formdata, no arrojará un error.

En el primer caso, el error está dentro de displayAjax()debajo this.statusTextcomo Method not Allowed.

En el segundo caso, simplemente funciona. Debe verificar en el lado del servidor si pasó los datos de publicación correctos.

el dominio cruzado no permitido arroja el error automáticamente.

En la respuesta de error, no hay códigos de error.

Solo existe el this.typeque está configurado como error.

¿Por qué agregar un controlador de errores si no tiene control sobre los errores? La mayoría de los errores se devuelven dentro de esto en la función de devolución de llamada displayAjax().

Por lo tanto: no es necesario realizar comprobaciones de errores si puede copiar y pegar la URL correctamente. ;)

PD: Como la primera prueba, escribí x ('x', displayAjax) ..., y obtuve una respuesta ... ¿??? Así que verifiqué la carpeta donde se encuentra el HTML y había un archivo llamado 'x.xml'. Entonces, incluso si olvida la extensión de su archivo, XMLHttpRequest 2 LO ENCONTRARÁ . Yo jajaja


Leer un archivo sincrónico

No hagas eso.

Si desea bloquear el navegador por un tiempo, cargue un gran .txtarchivo síncrono.

function omg(a, c){ // URL
  c = new XMLHttpRequest;
  c.open('GET', a, true);
  c.send();
  return c; // Or c.response
}

Ahora puedes hacer

 var res = omg('thisIsGonnaBlockThePage.txt');

No hay otra forma de hacer esto de una manera no asincrónica. (Sí, con el ciclo setTimeout ... pero en serio?)

Otro punto es ... si trabaja con API o solo los archivos de su propia lista o lo que sea, siempre usa diferentes funciones para cada solicitud ...

Solo si tiene una página donde carga siempre el mismo XML / JSON o lo que sea, solo necesita una función. En ese caso, modifique un poco la función Ajax y reemplace b con su función especial.


Las funciones anteriores son para uso básico.

Si desea EXTENDER la función ...

Sí tu puedes.

Estoy usando muchas API y una de las primeras funciones que integro en cada página HTML es la primera función Ajax en esta respuesta, solo con GET ...

Pero puedes hacer muchas cosas con XMLHttpRequest 2:

Hice un administrador de descargas (usando rangos en ambos lados con currículum, lector de archivos, sistema de archivos), varios convertidores de redimensionadores de imágenes usando lienzo, poblar bases de datos SQL web con imágenes base64 y mucho más ... Pero en estos casos, debe crear una función solo para eso propósito ... a veces necesitas un blob, búferes de matriz, puedes configurar encabezados, anular el tipo mime y hay mucho más ...

Pero la pregunta aquí es cómo devolver una respuesta Ajax ... (agregué una manera fácil).


15
Si bien esta respuesta es agradable (y a todos nos encanta XHR2 y publicar datos de archivos y datos multiparte es totalmente increíble), esto muestra el azúcar sintáctico para publicar XHR con JavaScript, es posible que desee poner esto en una publicación de blog (me gustaría) o incluso en una biblioteca (no estoy seguro sobre el nombre x, ajaxo xhrpodría ser mejor :)). No veo cómo se trata de devolver la respuesta de una llamada AJAX. (alguien aún podría hacerlo var res = x("url")y no entender por qué no funciona;)). En una nota al margen: sería genial si volvieras cdel método para que los usuarios puedan conectarse, erroretc.
Benjamin Gruenbaum

25
2.ajax is meant to be async.. so NO var res=x('url')..Ese es el punto de esta pregunta y respuestas :)
Benjamin Gruenbaum

3
¿por qué hay un parámetro 'c' en las funciones, si en la primera línea está sobrescribiendo el valor que tenía? ¿Me estoy perdiendo de algo?
Brian H.

2
Puede usar parámetros como marcador de posición para evitar escribir varias veces "var"
cocco

11
@cocco ¿Entonces escribiste código engañoso e ilegible en una respuesta SO para ahorrar algunas pulsaciones de teclas? Por favor no hagas eso.
piedra

316

Si está usando promesas, esta respuesta es para usted.

Esto significa AngularJS, jQuery (con diferido), reemplazo nativo de XHR (búsqueda), EmberJS, guardado de BackboneJS o cualquier biblioteca de nodos que devuelva promesas.

Su código debe ser algo similar a esto:

function foo() {
    var data;
    // or $.get(...).then, or request(...).then, or query(...).then
    fetch("/echo/json").then(function(response){
        data = response.json();
    });
    return data;
}

var result = foo(); // result is always undefined no matter what.

Felix Kling hizo un buen trabajo escribiendo una respuesta para las personas que usan jQuery con devoluciones de llamada para AJAX. Tengo una respuesta para XHR nativo. Esta respuesta es para el uso genérico de las promesas, ya sea en la interfaz o en el backend.


El problema central

El modelo de concurrencia de JavaScript en el navegador y en el servidor con NodeJS / io.js es asíncrono y reactivo .

Cada vez que llama a un método que devuelve una promesa, los thencontroladores siempre se ejecutan de forma asíncrona, es decir, después del código debajo de ellos que no está en un.then controlador.

Esto significa que cuando devuelva dataelthen controlador que ha definido aún no se ejecutó. Esto a su vez significa que el valor que está devolviendo no se ha establecido en el valor correcto a tiempo.

Aquí hay una analogía simple para el problema:

    function getFive(){
        var data;
        setTimeout(function(){ // set a timer for one second in the future
           data = 5; // after a second, do this
        }, 1000);
        return data;
    }
    document.body.innerHTML = getFive(); // `undefined` here and not 5

El valor de dataes undefineddesde eldata = 5 parte aún no se ha ejecutado. Probablemente se ejecutará en un segundo, pero para ese momento ya no es relevante para el valor devuelto.

Dado que la operación aún no sucedió (AJAX, llamada al servidor, IO, temporizador), está devolviendo el valor antes de que la solicitud tenga la oportunidad de decirle a su código cuál es ese valor.

Una posible solución a este problema es codificar de forma reactiva , diciéndole a su programa qué hacer cuando se complete el cálculo. Las promesas permiten esto activamente al ser temporal (sensible al tiempo) en la naturaleza.

Resumen rápido de las promesas

Una promesa es un valor en el tiempo . Las promesas tienen estado, comienzan como pendientes sin valor y pueden conformarse con:

  • cumplido, lo que significa que el cálculo se completó con éxito.
  • rechazado, lo que significa que el cálculo falló.

Una promesa solo puede cambiar de estado una vez, después de lo cual siempre permanecerá en el mismo estado para siempre. Puede adjuntar thencontroladores a las promesas para extraer su valor y manejar los errores. thenLos manejadores permiten el encadenamiento de llamadas. Las promesas se crean mediante el uso de API que las devuelven . Por ejemplo, el reemplazo más moderno de AJAX fetcho las $.getpromesas de devolución de jQuery .

Cuando pedimos .thenuna promesa y devolvemos algo de ella, recibimos una promesa por el valor procesado . Si devolvemos otra promesa, obtendremos cosas increíbles, pero sostengamos nuestros caballos.

Con promesas

Veamos cómo podemos resolver el problema anterior con promesas. Primero, demostremos nuestra comprensión de los estados de promesa desde arriba usando el constructor Promise para crear una función de retraso:

function delay(ms){ // takes amount of milliseconds
    // returns a new promise
    return new Promise(function(resolve, reject){
        setTimeout(function(){ // when the time is up
            resolve(); // change the promise to the fulfilled state
        }, ms);
    });
}

Ahora, después de convertir setTimeout para usar promesas, podemos usarlo thenpara que cuente:

function delay(ms){ // takes amount of milliseconds
  // returns a new promise
  return new Promise(function(resolve, reject){
    setTimeout(function(){ // when the time is up
      resolve(); // change the promise to the fulfilled state
    }, ms);
  });
}

function getFive(){
  // we're RETURNING the promise, remember, a promise is a wrapper over our value
  return delay(100).then(function(){ // when the promise is ready
      return 5; // return the value 5, promises are all about return values
  })
}
// we _have_ to wrap it like this in the call site, we can't access the plain value
getFive().then(function(five){ 
   document.body.innerHTML = five;
});

Básicamente, en lugar de devolver un valor que no se puede hacer debido a la modelo de concurrencia - estamos devolviendo un envoltorio para un valor que podemos desenvolver con then. Es como una caja con la que puedes abrir then.

Aplicando esto

Esto es lo mismo para su llamada API original, puede:

function foo() {
    // RETURN the promise
    return fetch("/echo/json").then(function(response){
        return response.json(); // process it inside the `then`
    });
}

foo().then(function(response){
    // access the value inside the `then`
})

Entonces esto funciona igual de bien. Hemos aprendido que no podemos devolver valores de llamadas ya asincrónicas, pero podemos usar promesas y encadenarlas para realizar el procesamiento. Ahora sabemos cómo devolver la respuesta de una llamada asincrónica.

ES2015 (ES6)

ES6 introduce generadores que son funciones que pueden regresar en el medio y luego reanudar el punto en el que estaban. Esto suele ser útil para secuencias, por ejemplo:

function* foo(){ // notice the star, this is ES6 so new browsers/node/io only
    yield 1;
    yield 2;
    while(true) yield 3;
}

Es una función que devuelve un iterador sobre la secuencia 1,2,3,3,3,3,....que se puede iterar. Si bien esto es interesante por sí solo y abre muchas posibilidades, hay un caso interesante en particular.

Si la secuencia que estamos produciendo es una secuencia de acciones en lugar de números, podemos pausar la función cada vez que se produce una acción y esperarla antes de reanudar la función. Entonces, en lugar de una secuencia de números, necesitamos una secuencia de futuro valores , es decir, promesas.

Este truco algo complicado pero muy poderoso nos permite escribir código asincrónico de manera sincrónica. Hay varios "corredores" que hacen esto por usted, escribir uno es unas pocas líneas de código pero está más allá del alcance de esta respuesta. Usaré Bluebird's Promise.coroutineaquí, pero hay otros envoltorios como coo Q.async.

var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // notice the yield
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
});

Este método devuelve una promesa en sí, que podemos consumir de otras corutinas. Por ejemplo:

var main = coroutine(function*(){
   var bar = yield foo(); // wait our earlier coroutine, it returns a promise
   // server call done here, code below executes when done
   var baz = yield fetch("/api/users/"+bar.userid); // depends on foo's result
   console.log(baz); // runs after both requests done
});
main();

ES2016 (ES7)

En ES7, esto está más estandarizado, hay varias propuestas en este momento, pero en todas puedes awaitprometer. Esto es solo "azúcar" (mejor sintaxis) para la propuesta ES6 anterior agregando las palabras clave asyncy await. Haciendo el ejemplo anterior:

async function foo(){
    var data = await fetch("/echo/json"); // notice the await
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
}

Todavía devuelve una promesa igual :)


Esta debería ser la respuesta aceptada. +1 para asíncrono / espera (¿aunque no deberíamos return await data.json();?)
Lewis Donovan el

247

Estás utilizando Ajax incorrectamente. La idea no es que devuelva nada, sino que entregue los datos a algo llamado función de devolución de llamada, que maneja los datos.

Es decir:

function handleData( responseData ) {

    // Do what you want with the data
    console.log(responseData);
}

$.ajax({
    url: "hi.php",
    ...
    success: function ( data, status, XHR ) {
        handleData(data);
    }
});

Devolver cualquier cosa en el controlador de envío no hará nada. En su lugar, debe entregar los datos o hacer lo que quiera con ellos directamente dentro de la función de éxito.


13
Esta respuesta es completamente semántica ... su método de éxito es solo una devolución de llamada dentro de una devolución de llamada. Podrías haberlo hecho success: handleDatay funcionaría.
Jacques ジ ャ ッ ク

55
¿Y qué pasa si desea devolver el "responseData" fuera de "handleData" ... :) ... ¿cómo lo hará ...? ... porque un simple retorno lo devolverá a la devolución de llamada "éxito" del ajax ... y no fuera de "handleData" ...
pesho hristov

@Jacques & @pesho hristov Te perdiste este punto. El controlador de envío no es el successmétodo, es el alcance que lo rodea $.ajax.
travnik

@travnik No me lo perdí. Si tomas el contenido de handleData y lo pones en el método de éxito, actuaría exactamente igual ...
Jacques ジ ャ ッ ク

234

La solución más simple es crear una función de JavaScript y llamarla para la successdevolución de llamada de Ajax .

function callServerAsync(){
    $.ajax({
        url: '...',
        success: function(response) {

            successCallback(response);
        }
    });
}

function successCallback(responseObj){
    // Do something like read the response and show data
    alert(JSON.stringify(responseObj)); // Only applicable to JSON response
}

function foo(callback) {

    $.ajax({
        url: '...',
        success: function(response) {
           return callback(null, response);
        }
    });
}

var result = foo(function(err, result){
          if (!err)
           console.log(result);    
}); 

3
No sé quién lo votó negativo. Pero esta es una solución que ha funcionado, de hecho, utilicé este enfoque para crear una aplicación completa. El jquery.ajax no devuelve datos, por lo que es mejor usar el enfoque anterior. Si está mal, explique y sugiera una mejor manera de hacerlo.
Hemant Bavle

11
Lo siento, olvidé dejar un comentario (¡generalmente lo hago!). Lo rechacé. Los votos negativos no indican la corrección o la falta de hechos, sino la utilidad en el contexto o la falta de. No encuentro útil su respuesta, dada la de Felix, que ya explica esto solo con mucho más detalle. En una nota al margen, ¿por qué encadenarías la respuesta si es JSON?
Benjamin Gruenbaum

55
ok .. @Benjamin utilicé stringify, para convertir un objeto JSON en cadena. Y gracias por aclarar tu punto. Tendrá en cuenta para publicar respuestas más elaboradas.
Hemant Bavle

¿Y qué pasa si desea devolver el "responseObj" fuera de "successCallback" ... :) ... ¿cómo lo hará ...? ... porque un simple retorno lo devolverá a la devolución de llamada de "éxito" del ajax ... y no fuera de "successCallback" ...
pesho hristov

221

Contestaré con un cómic de aspecto horrible y dibujado a mano. La segunda imagen es la razón por la cual resultestá undefineden su ejemplo de código.

ingrese la descripción de la imagen aquí


32
Una imagen vale más que mil palabras , Persona A : Solicite detalles de la persona B para arreglar su automóvil, a su vez Persona B : Realiza una llamada Ajax y espera la respuesta del servidor para obtener detalles de la reparación del automóvil, cuando se recibe la respuesta, la función Ajax Success llama a la Persona B funciona y le pasa la respuesta como argumento, la persona A recibe la respuesta.
shaijut

10
Sería genial si agregara líneas de código con cada imagen para ilustrar los conceptos.
Hassan Baig

1
Mientras tanto, el tipo con el auto está atrapado a un lado de la carretera. Se requiere el coche se fija antes de continuar. Ahora está solo al costado del camino esperando ... Preferiría estar al teléfono esperando cambios de estado, pero el mecánico no lo hizo ... El mecánico dijo que tiene que continuar con su trabajo y no puede simplemente pasar el rato en el teléfono. Mechanic prometió que lo llamaría tan pronto como pudiera. Después de aproximadamente 4 horas, el chico se da por vencido y llama a Uber. - Ejemplo de tiempo de espera.
barrypicker

@barrypicker :-D ¡Brillante!
Johannes Fahrenkrug

159

Angular1

Para las personas que usan AngularJS , pueden manejar esta situación usando Promises.

Aquí dice

Las promesas se pueden usar para anular funciones asincrónicas y le permiten encadenar múltiples funciones.

Puedes encontrar una buena explicación aquí también.

Ejemplo encontrado en los documentos mencionados a continuación.

  promiseB = promiseA.then(
    function onSuccess(result) {
      return result + 1;
    }
    ,function onError(err) {
      //Handle error
    }
  );

 // promiseB will be resolved immediately after promiseA is resolved 
 // and its value will be the result of promiseA incremented by 1.

Angular2 y posterior

Con Angular2mira el siguiente ejemplo, pero se recomienda usar Observablescon Angular2.

 search(term: string) {
     return this.http
  .get(`https://api.spotify.com/v1/search?q=${term}&type=artist`)
  .map((response) => response.json())
  .toPromise();

}

Puedes consumir eso de esta manera,

search() {
    this.searchService.search(this.searchField.value)
      .then((result) => {
    this.result = result.artists.items;
  })
  .catch((error) => console.error(error));
}

Vea la publicación original aquí. Pero Typecript no es compatible con las promesas nativas de es6 , si desea usarlo, es posible que necesite un complemento para eso.

Además, aquí están las promesas que las especificaciones definen aquí.


15
Sin embargo, esto no explica cómo las promesas resolverían este problema.
Benjamin Gruenbaum

44
Los métodos jQuery y fetch también devuelven promesas. Sugeriría revisar su respuesta. Aunque jQuery's no es exactamente lo mismo (entonces está allí, pero catch no lo es).
Rastreador1

153

La mayoría de las respuestas aquí dan sugerencias útiles para cuando tiene una sola operación asíncrona, pero a veces, esto surge cuando necesita hacer una operación asincrónica para cada entrada en una matriz u otra estructura similar a una lista. La tentación es hacer esto:

// WRONG
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log(results); // E.g., using them, returning them, etc.

Ejemplo:

La razón por la que no funciona es que las devoluciones de llamada doSomethingAsyncaún no se han ejecutado cuando intenta usar los resultados.

Entonces, si tiene una matriz (o una lista de algún tipo) y desea realizar operaciones asíncronas para cada entrada, tiene dos opciones: Realizar las operaciones en paralelo (superposición) o en serie (una tras otra en secuencia).

Paralela

Puede iniciarlos todos y realizar un seguimiento de la cantidad de devoluciones de llamada que espera, y luego usar los resultados cuando haya recibido tantas devoluciones de llamada:

var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

Ejemplo:

(Podríamos eliminar expectingy simplemente usar results.length === theArray.length, pero eso nos deja abiertos a la posibilidad de quetheArray se cambie mientras las llamadas están pendientes ...)

Observe cómo usamos indexfrom forEachpara guardar el resultado enresults la misma posición que la entrada con la que se relaciona, incluso si los resultados llegan fuera de orden (dado que las llamadas asíncronas no necesariamente se completan en el orden en que se iniciaron).

Pero, ¿qué pasa si necesita devolver esos resultados de una función? Como las otras respuestas han señalado, no puedes; debe hacer que su función acepte y llame una devolución de llamada (o devuelva una Promesa ). Aquí hay una versión de devolución de llamada:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

Ejemplo:

O aquí hay una versión que devuelve un Promiseen su lugar:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Por supuesto, si doSomethingAsyncnos pasaran errores, usaríamosreject para rechazar la promesa cuando recibimos un error).

Ejemplo:

(O, alternativamente, puede hacer un envoltorio para doSomethingAsynceso devuelve una promesa, y luego hacer lo siguiente ...)

Si doSomethingAsyncle da una promesa , puede usar Promise.all:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry);
    }));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Si sabe que doSomethingAsyncignorará un segundo y tercer argumento, puede pasarlo directamente a map( mapllama a su devolución de llamada con tres argumentos, pero la mayoría de las personas solo usa el primero la mayor parte del tiempo):

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Ejemplo:

Tenga en cuenta que Promise.allresuelve su promesa con una serie de resultados de todas las promesas que le hace cuando se resuelven todas, o rechaza su promesa cuando la primera de las promesas que le da rechaza.

Serie

¿Y si no quieres que las operaciones sean paralelas? Si desea ejecutarlos uno tras otro, debe esperar a que se complete cada operación antes de comenzar la siguiente. Aquí hay un ejemplo de una función que hace eso y llama a una devolución de llamada con el resultado:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

(Como estamos haciendo el trabajo en serie, podemos usarlo results.push(result)porque sabemos que no obtendremos resultados fuera de orden. En lo anterior podríamos haber usado results[index] = result;, pero en algunos de los siguientes ejemplos no tenemos un índice usar.)

Ejemplo:

(O, de nuevo, construya un envoltorio para doSomethingAsynceso le promete y haga lo siguiente ...)

Si doSomethingAsyncle da una promesa, si puede usar la sintaxis ES2017 + (tal vez con un transpilador como Babel ), puede usar una asyncfunción con for-ofy await:

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Ejemplo:

Si no puede usar la sintaxis ES2017 + (todavía), puede usar una variación en el patrón " Reducción de promesa" (esto es más complejo que la reducción de Promesa habitual porque no estamos pasando el resultado de uno a otro, sino que reuniendo sus resultados en una matriz):

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Ejemplo:

... que es menos engorroso con las funciones de flecha ES2015 + :

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Ejemplo:


1
¿Podría explicar cómo funciona la if (--expecting === 0)parte del código, por favor? La versión de devolución de llamada de su solución está funcionando muy bien para mí, simplemente no entiendo cómo, con esa declaración, está verificando la cantidad de respuestas completadas. Aprecio que es solo falta de conocimiento de mi parte. ¿Hay alguna forma alternativa de escribir ese cheque?
Sarah

@Sarah: expectingcomienza con el valor de array.length, que es cuántas solicitudes vamos a hacer. Sabemos que la devolución de llamada no se llamará hasta que se inicien todas esas solicitudes. En la devolución de llamada, if (--expecting === 0)hace esto: 1. Disminuciones expecting(hemos recibido una respuesta, por lo que esperamos una respuesta menos) y si el valor después de la disminución es 0 (no esperamos más respuestas), estamos ¡hecho!
TJ Crowder

1
@PatrickRoberts - ¡Gracias! Sí, error de copiar y pegar, ese segundo argumento fue completamente ignorado en ese ejemplo (que es la única razón por la que no falló, ya que, como señaló, resultsno existía). :-) Arreglado.
TJ Crowder

111

Echa un vistazo a este ejemplo:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope,$http) {

    var getJoke = function(){
        return $http.get('http://api.icndb.com/jokes/random').then(function(res){
            return res.data.value;  
        });
    }

    getJoke().then(function(res) {
        console.log(res.joke);
    });
});

Como puede ver, getJokeestá devolviendo una promesa resuelta (se resuelve al regresar res.data.value). Por lo tanto, espere hasta que se complete la solicitud $ http.get y luego se ejecute console.log (res.joke) (como un flujo asincrónico normal).

Este es el plnkr:

http://embed.plnkr.co/XlNR7HpCaIhJxskMJfSg/

Forma ES6 (asíncrono - espera)

(function(){
  async function getJoke(){
    let response = await fetch('http://api.icndb.com/jokes/random');
    let data = await response.json();
    return data.value;
  }

  getJoke().then((joke) => {
    console.log(joke);
  });
})();

107

Este es uno de los lugares que vinculan los datos de dos maneras o almacenan el concepto que se utilizan en muchos nuevos marcos de JavaScript funcionarán muy bien para usted ...

Entonces, si está utilizando Angular, React o cualquier otro marco que haga dos formas de vincular datos o almacenar el concepto, este problema simplemente se soluciona para usted, por lo que, en pocas palabras, su resultado está undefineden la primera etapa, por lo que tieneresult = undefined antes de recibir el datos, luego, tan pronto como obtenga el resultado, se actualizará y se asignará al nuevo valor cuya respuesta de su llamada Ajax ...

Pero, ¿cómo puedes hacerlo en javascript puro o jQuery? por ejemplo, como hizo en esta pregunta?

Puede usar una devolución de llamada , promesa y recientemente observable para manejarlo por usted, por ejemplo, en promesas que tenemos alguna función como success()o then()que se ejecutará cuando sus datos estén listos para usted, lo mismo con la función de devolución de llamada o suscripción en observable .

Por ejemplo, en su caso en el que está utilizando jQuery , puede hacer algo como esto:

$(document).ready(function(){
    function foo() {
        $.ajax({url: "api/data", success: function(data){
            fooDone(data); //after we have data, we pass it to fooDone
        }});
    };

    function fooDone(data) {
        console.log(data); //fooDone has the data and console.log it
    };

    foo(); //call happens here
});

Para obtener más información, estudie las promesas y los observables, que son nuevas formas de hacer esto de manera asíncrona.


Esto está bien en el ámbito global, pero en el contexto de algún módulo probablemente desee asegurar el contexto adecuado para la devolución de llamada, por ejemplo$.ajax({url: "api/data", success: fooDone.bind(this)});
steve.sims

8
Esto es realmente incorrecto ya que React es un enlace de datos unidireccional
Matthew Brent

@MatthewBrent no estás equivocado, pero tampoco está bien, los accesorios React son objeto y si se cambian, cambian a lo largo de la aplicación, pero no es una forma en que el desarrollador React recomienda usarlo ...
Alireza

98

Es un problema muy común que enfrentamos mientras luchamos con los 'misterios' de JavaScript. Déjame intentar desmitificar este misterio hoy.

Comencemos con una simple función de JavaScript:

function foo(){
// do something 
 return 'wohoo';
}

let bar = foo(); // bar is 'wohoo' here

Esa es una simple llamada de función síncrona (donde cada línea de código está 'terminada con su trabajo' antes de la siguiente en secuencia), y el resultado es el mismo que se esperaba.

Ahora agreguemos un poco de giro, introduciendo un pequeño retraso en nuestra función, para que todas las líneas de código no estén 'terminadas' en secuencia. Por lo tanto, emulará el comportamiento asincrónico de la función:

function foo(){
 setTimeout( ()=>{
   return 'wohoo';
  }, 1000 )
}

let bar = foo() // bar is undefined here

¡Ahí tienes, ese retraso simplemente rompió la funcionalidad que esperábamos! ¿Pero qué pasó exactamente? Bueno, en realidad es bastante lógico si nos fijamos en el código. la función foo(), después de la ejecución, no devuelve nada (por lo tanto, el valor devuelto es undefined), pero inicia un temporizador, que ejecuta una función después de 1s para devolver 'wohoo'. Pero como puede ver, el valor asignado a la barra es el material devuelto de inmediato por foo (), que no es nada, es decir, soloundefined .

Entonces, ¿cómo abordamos este problema?

Pidamos a nuestra función una PROMESA . Promise se trata realmente de lo que significa: significa que la función le garantiza proporcionar cualquier salida que obtenga en el futuro. así que vamos a verlo en acción para nuestro pequeño problema anterior:

function foo(){
   return new Promise( (resolve, reject) => { // I want foo() to PROMISE me something
    setTimeout ( function(){ 
      // promise is RESOLVED , when execution reaches this line of code
       resolve('wohoo')// After 1 second, RESOLVE the promise with value 'wohoo'
    }, 1000 )
  })
}

let bar ; 
foo().then( res => {
 bar = res;
 console.log(bar) // will print 'wohoo'
});

Por lo tanto, el resumen es: para abordar las funciones asincrónicas como llamadas basadas en ajax, etc., puede usar una promesa sobre resolveel valor (que tiene la intención de devolver). Por lo tanto, en resumen, resuelve el valor en lugar de devolverlo en funciones asincrónicas.

ACTUALIZACIÓN (Promesas con asíncrono / espera)

Además de usar then/catchpara trabajar con promesas, existe un enfoque más. La idea es reconocer una función asincrónica y luego esperar a que se resuelvan las promesas , antes de pasar a la siguiente línea de código. Sigue siendo solo elpromises debajo del capó, pero con un enfoque sintáctico diferente. Para aclarar las cosas, puede encontrar una comparación a continuación:

entonces / captura versión:

function saveUsers(){
     getUsers()
      .then(users => {
         saveSomewhere(users);
      })
      .catch(err => {
          console.error(err);
       })
 }

versión asíncrona / espera:

  async function saveUsers(){
     try{
        let users = await getUsers()
        saveSomewhere(users);
     }
     catch(err){
        console.error(err);
     }
  }

¿Sigue siendo considerada la mejor manera de devolver un valor de una promesa o asíncrono / espera?
edwardsmarkf

3
@edwardsmarkf Personalmente no creo que haya una mejor manera como tal. Utilizo promesas con then / catch, async / await y generadores para partes asíncronas de mi código. Depende en gran medida del contexto de uso.
Anish K.

96

Otro enfoque para devolver un valor de una función asincrónica es pasar un objeto que almacenará el resultado de la función asincrónica.

Aquí hay un ejemplo de lo mismo:

var async = require("async");

// This wires up result back to the caller
var result = {};
var asyncTasks = [];
asyncTasks.push(function(_callback){
    // some asynchronous operation
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;
            _callback();
        }
    });
});

async.parallel(asyncTasks, function(){
    // result is available after performing asynchronous operation
    console.log(result)
    console.log('Done');
});

Estoy usando el result objeto para almacenar el valor durante la operación asincrónica. Esto permite que el resultado esté disponible incluso después del trabajo asincrónico.

Yo uso mucho este enfoque. Me interesaría saber qué tan bien funciona este enfoque donde se involucra el cableado del resultado a través de módulos consecutivos.


99
No hay nada especial sobre el uso de un objeto aquí. Funcionaría tan bien si le asignaras la respuesta directamente result. Funciona porque está leyendo la variable después de que se completa la función asíncrona.
Felix Kling

85

Si bien las promesas y las devoluciones de llamada funcionan bien en muchas situaciones, es difícil expresar algo como:

if (!name) {
  name = async1();
}
async2(name);

Terminarías atravesando async1; compruebe si nameestá indefinido o no y llame a la devolución de llamada en consecuencia.

async1(name, callback) {
  if (name)
    callback(name)
  else {
    doSomething(callback)
  }
}

async1(name, async2)

Si bien está bien en pequeños ejemplos, se vuelve molesto cuando tiene muchos casos similares y manejo de errores involucrado.

Fibers Ayuda a resolver el problema.

var Fiber = require('fibers')

function async1(container) {
  var current = Fiber.current
  var result
  doSomething(function(name) {
    result = name
    fiber.run()
  })
  Fiber.yield()
  return result
}

Fiber(function() {
  var name
  if (!name) {
    name = async1()
  }
  async2(name)
  // Make any number of async calls from here
}

Puedes ver el proyecto aquí .


1
@recurf - No es mi proyecto. Podría intentar usar su rastreador de problemas.
rohithpr

1
¿Es esto similar a las funciones del generador? developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… *
Emanegux

1
¿Sigue siendo relevante?
Aluan Haddad

Puede utilizar async-awaitsi está utilizando algunas de las versiones más recientes de node. Si alguien está atascado con versiones anteriores, puede usar este método.
rohithpr

83

El siguiente ejemplo que he escrito muestra cómo

  • Manejar llamadas HTTP asincrónicas;
  • Espere la respuesta de cada llamada a la API;
  • Usar patrón de promesa ;
  • Use el patrón Promise.all para unir múltiples llamadas HTTP;

Este ejemplo de trabajo es autónomo. Definirá un objeto de solicitud simple que usa el XMLHttpRequestobjeto de ventana para realizar llamadas. Definirá una función simple para esperar a que se completen un montón de promesas.

Contexto. El ejemplo es consultar el punto final de la API web de Spotify para buscar playlistobjetos para un conjunto dado de cadenas de consulta:

[
 "search?type=playlist&q=%22doom%20metal%22",
 "search?type=playlist&q=Adele"
]

Para cada elemento, una nueva Promesa disparará un bloque - ExecutionBlock, analizará el resultado, programará un nuevo conjunto de promesas en función de la matriz de resultados, es decir, una lista de userobjetos de Spotify y ejecutará la nueva llamada HTTP dentro del ExecutionProfileBlockasíncrono.

Luego puede ver una estructura de Promesa anidada, que le permite generar múltiples llamadas HTTP anidadas completamente asíncronas, y unir los resultados de cada subconjunto de llamadas Promise.all.

NOTA Las searchAPI de Spotify recientes requerirán que se especifique un token de acceso en los encabezados de solicitud:

-H "Authorization: Bearer {your access token}" 

Entonces, para ejecutar el siguiente ejemplo, debe colocar su token de acceso en los encabezados de solicitud:

var spotifyAccessToken = "YourSpotifyAccessToken";
var console = {
    log: function(s) {
        document.getElementById("console").innerHTML += s + "<br/>"
    }
}

// Simple XMLHttpRequest
// based on https://davidwalsh.name/xmlhttprequest
SimpleRequest = {
    call: function(what, response) {
        var request;
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            request = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // Internet Explorer
            try {
                request = new ActiveXObject('Msxml2.XMLHTTP');
            }
            catch (e) {
                try {
                  request = new ActiveXObject('Microsoft.XMLHTTP');
                } catch (e) {}
            }
        }

        // State changes
        request.onreadystatechange = function() {
            if (request.readyState === 4) { // Done
                if (request.status === 200) { // Complete
                    response(request.responseText)
                }
                else
                    response();
            }
        }
        request.open('GET', what, true);
        request.setRequestHeader("Authorization", "Bearer " + spotifyAccessToken);
        request.send(null);
    }
}

//PromiseAll
var promiseAll = function(items, block, done, fail) {
    var self = this;
    var promises = [],
                   index = 0;
    items.forEach(function(item) {
        promises.push(function(item, i) {
            return new Promise(function(resolve, reject) {
                if (block) {
                    block.apply(this, [item, index, resolve, reject]);
                }
            });
        }(item, ++index))
    });
    Promise.all(promises).then(function AcceptHandler(results) {
        if (done) done(results);
    }, function ErrorHandler(error) {
        if (fail) fail(error);
    });
}; //promiseAll

// LP: deferred execution block
var ExecutionBlock = function(item, index, resolve, reject) {
    var url = "https://api.spotify.com/v1/"
    url += item;
    console.log( url )
    SimpleRequest.call(url, function(result) {
        if (result) {

            var profileUrls = JSON.parse(result).playlists.items.map(function(item, index) {
                return item.owner.href;
            })
            resolve(profileUrls);
        }
        else {
            reject(new Error("call error"));
        }
    })
}

arr = [
    "search?type=playlist&q=%22doom%20metal%22",
    "search?type=playlist&q=Adele"
]

promiseAll(arr, function(item, index, resolve, reject) {
    console.log("Making request [" + index + "]")
    ExecutionBlock(item, index, resolve, reject);
}, function(results) { // Aggregated results

    console.log("All profiles received " + results.length);
    //console.log(JSON.stringify(results[0], null, 2));

    ///// promiseall again

    var ExecutionProfileBlock = function(item, index, resolve, reject) {
        SimpleRequest.call(item, function(result) {
            if (result) {
                var obj = JSON.parse(result);
                resolve({
                    name: obj.display_name,
                    followers: obj.followers.total,
                    url: obj.href
                });
            } //result
        })
    } //ExecutionProfileBlock

    promiseAll(results[0], function(item, index, resolve, reject) {
        //console.log("Making request [" + index + "] " + item)
        ExecutionProfileBlock(item, index, resolve, reject);
    }, function(results) { // aggregated results
        console.log("All response received " + results.length);
        console.log(JSON.stringify(results, null, 2));
    }

    , function(error) { // Error
        console.log(error);
    })

    /////

  },
  function(error) { // Error
      console.log(error);
  });
<div id="console" />

He discutido ampliamente esta solución aquí .


80

La respuesta corta es que debe implementar una devolución de llamada como esta:

function callback(response) {
    // Here you can do what ever you want with the response object.
    console.log(response);
}

$.ajax({
    url: "...",
    success: callback
});

78

Respuesta de 2017: ahora puede hacer exactamente lo que quiere en cada navegador y nodo actual

Esto es bastante simple:

  • Devolver una promesa
  • Use 'esperar' , que le indicará a JavaScript que espere la promesa de resolverse en un valor (como la respuesta HTTP)
  • Agregue la palabra clave 'async' a la función padre

Aquí hay una versión funcional de su código:

(async function(){

var response = await superagent.get('...')
console.log(response)

})()

esperar es compatible con todos los navegadores actuales y el nodo 8


77
Desafortunadamente, esto solo funciona con funciones que devuelven promesas; por ejemplo, no funciona con la API Node.js, que utiliza devoluciones de llamada. Y no recomendaría usarlo sin Babel, porque no todos usan los "navegadores actuales".
Michał Perłakowski

2
@ MichałPerłakowski, el nodo 8 incluye nodejs.org/api/util.html#util_util_promisify_original que se puede usar para hacer que la API de node.js devuelva promesas. El hecho de que tenga el tiempo y el dinero para soportar navegadores no actuales obviamente depende de su situación.
mikemaccana

IE 11 sigue siendo un navegador actual en 2018, lamentablemente y no es compatibleawait/async
Juan Mendes

IE11 no es un navegador actual. Fue lanzado hace 5 años, tiene una participación de mercado mundial del 2.5% según caniuse, y a menos que alguien esté duplicando su presupuesto para ignorar toda la tecnología actual, entonces no vale la pena el tiempo de la mayoría de las personas.
mikemaccana

76

Js es un único subproceso.

El navegador se puede dividir en tres partes:

1) Bucle de eventos

2) API web

3) Cola de eventos

Event Loop se ejecuta para siempre, es decir, un tipo de bucle infinito. Event Queue es donde todas sus funciones se insertan en algún evento (ejemplo: clic), esto se realiza una por una fuera de la cola y se coloca en Event loop que ejecuta esta función y la prepara para el siguiente después de que se ejecute el primero. Esto significa que la ejecución de una función no comienza hasta que la función anterior a la cola se ejecute en el bucle de eventos.

Ahora pensemos que empujamos dos funciones en una cola, una es para obtener datos del servidor y otra utiliza esos datos. Primero empujamos la función serverRequest () en la cola y luego utilizamos la función Data (). La función serverRequest entra en el bucle de eventos y realiza una llamada al servidor, ya que nunca sabemos cuánto tiempo llevará obtener datos del servidor, por lo que se espera que este proceso tome tiempo y, por lo tanto, ocupamos nuestro bucle de eventos, colgando así nuestra página, ahí es donde Web API entra en función, toma esta función del bucle de eventos y trata con el servidor haciendo que el bucle de eventos sea gratuito para que podamos ejecutar la siguiente función desde la cola. La siguiente función en la cola es utiliseData () que va en bucle pero debido a que no hay datos disponibles, se va el desperdicio y la ejecución de la siguiente función continúa hasta el final de la cola (esto se llama llamada asíncrona, es decir, podemos hacer algo más hasta que obtengamos datos)

Supongamos que nuestra función serverRequest () tenía una declaración de retorno en un código, cuando recuperamos los datos de la API web del servidor la empujará a la cola al final de la cola. A medida que se empuja al final de la cola, no podemos utilizar sus datos, ya que no queda ninguna función en nuestra cola para utilizar estos datos. Por lo tanto, no es posible devolver algo de Async Call.

Por lo tanto, la solución a esto es la devolución de llamada o la promesa .

Una imagen de una de las respuestas aquí, explica correctamente el uso de la devolución de llamada ... Le damos a nuestra función (función que utiliza los datos devueltos por el servidor) a la función que llama al servidor.

Llamar de vuelta

 function doAjax(callbackFunc, method, url) {
  var xmlHttpReq = new XMLHttpRequest();
  xmlHttpReq.open(method, url);
  xmlHttpReq.onreadystatechange = function() {

      if (xmlHttpReq.readyState == 4 && xmlHttpReq.status == 200) {
        callbackFunc(xmlHttpReq.responseText);
      }


  }
  xmlHttpReq.send(null);

}

En mi código se llama como

function loadMyJson(categoryValue){
  if(categoryValue==="veg")
  doAjax(print,"GET","http://localhost:3004/vegetables");
  else if(categoryValue==="fruits")
  doAjax(print,"GET","http://localhost:3004/fruits");
  else 
  console.log("Data not found");
}

Devolución de llamada Javscript.info


68

Puede usar esta biblioteca personalizada (escrita usando Promise) para hacer una llamada remota.

function $http(apiConfig) {
    return new Promise(function (resolve, reject) {
        var client = new XMLHttpRequest();
        client.open(apiConfig.method, apiConfig.url);
        client.send();
        client.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                // Performs the function "resolve" when this.status is equal to 2xx.
                // Your logic here.
                resolve(this.response);
            }
            else {
                // Performs the function "reject" when this.status is different than 2xx.
                reject(this.statusText);
            }
        };
        client.onerror = function () {
            reject(this.statusText);
        };
    });
}

Ejemplo de uso simple:

$http({
    method: 'get',
    url: 'google.com'
}).then(function(response) {
    console.log(response);
}, function(error) {
    console.log(error)
});

67

Otra solución es ejecutar código a través del ejecutor secuencial nsynjs .

Si se promete la función subyacente

nsynjs evaluará todas las promesas secuencialmente y colocará el resultado de la promesa en datapropiedad:

function synchronousCode() {

    var getURL = function(url) {
        return window.fetch(url).data.text().data;
    };
    
    var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
    console.log('received bytes:',getURL(url).length);
    
};

nsynjs.run(synchronousCode,{},function(){
    console.log('synchronousCode done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

Si no se promete la función subyacente

Paso 1. Ajuste la función con devolución de llamada en un contenedor compatible con nsynjs (si tiene una versión prometida, puede omitir este paso):

var ajaxGet = function (ctx,url) {
    var res = {};
    var ex;
    $.ajax(url)
    .done(function (data) {
        res.data = data;
    })
    .fail(function(e) {
        ex = e;
    })
    .always(function() {
        ctx.resume(ex);
    });
    return res;
};
ajaxGet.nsynjsHasCallback = true;

Paso 2. Ponga en funcionamiento la lógica síncrona:

function process() {
    console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}

Paso 3. Ejecuta la función de manera síncrona a través de nsynjs:

nsynjs.run(process,this,function () {
    console.log("synchronous function finished");
});

Nsynjs evaluará todos los operadores y expresiones paso a paso, pausando la ejecución en caso de que el resultado de alguna función lenta no esté lista.

Más ejemplos aquí: https://github.com/amaksr/nsynjs/tree/master/examples


2
Esto es interesante. Me gusta cómo permite codificar llamadas asíncronas como lo haría en otros idiomas. ¿Pero técnicamente no es JavaScript real?
J Morris

41

ECMAScript 6 tiene 'generadores' que le permiten programar fácilmente en un estilo asincrónico.

function* myGenerator() {
    const callback = yield;
    let [response] = yield $.ajax("https://stackoverflow.com", {complete: callback});
    console.log("response is:", response);

    // examples of other things you can do
    yield setTimeout(callback, 1000);
    console.log("it delayed for 1000ms");
    while (response.statusText === "error") {
        [response] = yield* anotherGenerator();
    }
}

Para ejecutar el código anterior, haga esto:

const gen = myGenerator(); // Create generator
gen.next(); // Start it
gen.next((...args) => gen.next([...args])); // Set its callback function

Si necesita apuntar a navegadores que no admiten ES6, puede ejecutar el código a través de Babel o el compilador de cierre para generar ECMAScript 5.

Las devoluciones de llamada ...argsse envuelven en una matriz y se desestructuran cuando las lee para que el patrón pueda hacer frente a devoluciones de llamada que tienen múltiples argumentos. Por ejemplo con el nodo fs :

const [err, data] = yield fs.readFile(filePath, "utf-8", callback);

39

Aquí hay algunos enfoques para trabajar con solicitudes asincrónicas:

  1. Objeto Promesa del navegador
  2. Q : una biblioteca prometedora para JavaScript
  3. A + Promises.js
  4. jQuery diferido
  5. API XMLHttpRequest
  6. Uso del concepto de devolución de llamada: como implementación en la primera respuesta

Ejemplo: implementación diferida de jQuery para trabajar con múltiples solicitudes

var App = App || {};

App = {
    getDataFromServer: function(){

      var self = this,
                 deferred = $.Deferred(),
                 requests = [];

      requests.push($.getJSON('request/ajax/url/1'));
      requests.push($.getJSON('request/ajax/url/2'));

      $.when.apply(jQuery, requests).done(function(xhrResponse) {
        return deferred.resolve(xhrResponse.result);
      });
      return deferred;
    },

    init: function(){

        this.getDataFromServer().done(_.bind(function(resp1, resp2) {

           // Do the operations which you wanted to do when you
           // get a response from Ajax, for example, log response.
        }, this));
    }
};
App.init();


38

Nos encontramos en un universo que parece progresar a lo largo de una dimensión que llamamos "tiempo". Realmente no entendemos qué hora es, pero hemos desarrollado abstracciones y vocabulario que nos permiten razonar y hablar sobre ello: "pasado", "presente", "futuro", "antes", "después".

Los sistemas informáticos que construimos, cada vez más, tienen el tiempo como una dimensión importante. Ciertas cosas están preparadas para suceder en el futuro. Luego, otras cosas deben suceder después de que esas primeras cosas eventualmente ocurran. Esta es la noción básica llamada "asincronía". En nuestro mundo cada vez más conectado en red, el caso más común de asincronía es esperar que algún sistema remoto responda a alguna solicitud.

Considera un ejemplo. Llamas al lechero y pides un poco de leche. Cuando llegue, querrás ponerlo en tu café. No puedes poner la leche en tu café en este momento, porque todavía no está aquí. Tienes que esperar a que llegue antes de ponerlo en tu café. En otras palabras, lo siguiente no funcionará:

var milk = order_milk();
put_in_coffee(milk);

Debido a que JS no tiene forma de saber que necesita esperar a order_milkque termine antes de ejecutarse put_in_coffee. En otras palabras, no sabe que order_milkes asíncrono, es algo que no va a producir leche hasta algún momento futuro. JS y otros lenguajes declarativos ejecutan una declaración tras otra sin esperar.

El enfoque clásico de JS para este problema, aprovechando el hecho de que JS admite funciones como objetos de primera clase que se pueden pasar, es pasar una función como parámetro a la solicitud asincrónica, que luego invocará cuando se haya completado su tarea en algún momento en el futuro. Ese es el enfoque de "devolución de llamada". Se parece a esto:

order_milk(put_in_coffee);

order_milkcomienza, ordena la leche, luego, cuando y solo cuando llega, invoca put_in_coffee.

El problema con este enfoque de devolución de llamada es que contamina la semántica normal de una función que informa su resultado return; en cambio, las funciones no deben informar sus resultados llamando a una devolución de llamada dada como un parámetro. Además, este enfoque puede volverse difícil de manejar rápidamente cuando se trata de secuencias de eventos más largas. Por ejemplo, digamos que quiero esperar a que se ponga la leche en el café y luego, y solo entonces, realizar un tercer paso, es decir, beber el café. Termino necesitando escribir algo como esto:

order_milk(function(milk) { put_in_coffee(milk, drink_coffee); }

donde estoy pasando put_in_coffeetanto la leche para ponerla como la acción ( drink_coffee) para ejecutar una vez que se ha puesto la leche. Tal código se vuelve difícil de escribir, leer y depurar.

En este caso, podríamos reescribir el código en la pregunta como:

var answer;
$.ajax('/foo.json') . done(function(response) {
  callback(response.data);
});

function callback(data) {
  console.log(data);
}

Introduce promesas

Esta fue la motivación para la noción de una "promesa", que es un tipo particular de valor que representa un resultado futuro o asíncrono de algún tipo. Puede representar algo que ya sucedió, o que va a suceder en el futuro, o puede que nunca suceda. Las promesas tienen un único método, llamado then, al que pasa una acción para que se ejecute cuando el resultado que representa la promesa se ha cumplido.

En el caso de nuestra leche y café, diseñamos order_milkdevolver una promesa para la llegada de la leche, luego especificamos put_in_coffeecomo thenacción, de la siguiente manera:

order_milk() . then(put_in_coffee)

Una ventaja de esto es que podemos unirlos para crear secuencias de sucesos futuros ("encadenamiento"):

order_milk() . then(put_in_coffee) . then(drink_coffee)

Apliquemos promesas a su problema particular. Envolveremos nuestra lógica de solicitud dentro de una función, que devuelve una promesa:

function get_data() {
  return $.ajax('/foo.json');
}

En realidad, todo lo que hemos hecho es agregar returna la llamada a $.ajax. Esto funciona porque jQuery $.ajaxya devuelve una especie de promesa. (En la práctica, sin entrar en detalles, preferiríamos finalizar esta llamada para devolver una promesa real, o usar alguna alternativa a $.ajaxeso). Ahora, si queremos cargar el archivo y esperar a que termine y entonces hacer algo, simplemente podemos decir

get_data() . then(do_something)

por ejemplo,

get_data() . 
  then(function(data) { console.log(data); });

Cuando usamos promesas, terminamos pasando muchas funciones then, por lo que a menudo es útil usar las funciones de flecha de estilo ES6 más compactas:

get_data() . 
  then(data => console.log(data));

La asyncpalabra clave

Pero todavía hay algo vagamente insatisfactorio sobre tener que escribir código de una manera si es sincrónica y de una manera bastante diferente si es asíncrona. Para síncrono, escribimos

a();
b();

pero si aes asíncrono, con promesas tenemos que escribir

a() . then(b);

Arriba, dijimos, "JS no tiene forma de saber que necesita esperar a que termine la primera llamada antes de ejecutar la segunda". ¿No sería agradable si era alguna forma de saber que JS? Resulta que la hay, la awaitpalabra clave, utilizada dentro de un tipo especial de función llamada función "asíncrona". Esta característica es parte de la próxima versión de ES pero ya está disponible en transpiladores como Babel con los ajustes preestablecidos correctos. Esto nos permite simplemente escribir

async function morning_routine() {
  var milk   = await order_milk();
  var coffee = await put_in_coffee(milk);
  await drink(coffee);
}

En su caso, podría escribir algo como

async function foo() {
  data = await get_data();
  console.log(data);
}

37

Respuesta corta : su foo()método regresa inmediatamente, mientras que la $ajax()llamada se ejecuta de forma asíncrona después de que la función regresa . El problema es entonces cómo o dónde almacenar los resultados recuperados por la llamada asíncrona una vez que regresa.

Se han dado varias soluciones en este hilo. Quizás la forma más fácil es pasar un objeto al foo()método y almacenar los resultados en un miembro de ese objeto después de que se complete la llamada asíncrona.

function foo(result) {
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;   // Store the async result
        }
    });
}

var result = { response: null };   // Object to hold the async result
foo(result);                       // Returns before the async completes

Tenga en cuenta que la llamada a foo()aún no devolverá nada útil. Sin embargo, el resultado de la llamada asincrónica ahora se almacenará en result.response.


14
Si bien esto funciona, no es realmente mejor que asignar a una variable global.
Felix Kling

36

Use una callback()función dentro del foo()éxito. Intenta de esta manera. Es simple y fácil de entender.  

var lat = "";
var lon = "";
function callback(data) {
    lat = data.lat;
    lon = data.lon;
}
function getLoc() {
    var url = "http://ip-api.com/json"
    $.getJSON(url, function(data) {
        callback(data);
    });
}

getLoc();

29

La pregunta fue:

¿Cómo devuelvo la respuesta de una llamada asincrónica?

que PUEDE interpretarse como:

¿Cómo hacer que el código asincrónico se vea sincrónico ?

La solución será evitar devoluciones de llamada y usar una combinación de Promesas y asíncrono / espera .

Me gustaría dar un ejemplo para una solicitud de Ajax.

(Aunque puede escribirse en Javascript, prefiero escribirlo en Python y compilarlo en Javascript usando Transcrypt . Será lo suficientemente claro).

Primero habilitemos el uso de JQuery, para tener $disponible como S:

__pragma__ ('alias', 'S', '$')

Defina una función que devuelva una Promesa , en este caso una llamada Ajax:

def read(url: str):
    deferred = S.Deferred()
    S.ajax({'type': "POST", 'url': url, 'data': { },
        'success': lambda d: deferred.resolve(d),
        'error': lambda e: deferred.reject(e)
    })
    return deferred.promise()

Use el código asincrónico como si fuera síncrono :

async def readALot():
    try:
        result1 = await read("url_1")
        result2 = await read("url_2")
    except Exception:
        console.warn("Reading a lot failed")

29

Usando Promesa

La respuesta más perfecta a esta pregunta es usar Promise.

function ajax(method, url, params) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open(method, url);
    xhr.send(params);
  });
}

Uso

ajax("GET", "/test", "acrive=1").then(function(result) {
    // Code depending on result
})
.catch(function() {
    // An error occurred
});

Pero espera...!

¡Hay un problema con el uso de promesas!

¿Por qué deberíamos usar nuestra propia promesa personalizada?

Estuve usando esta solución por un tiempo hasta que descubrí que hay un error en los navegadores antiguos:

Uncaught ReferenceError: Promise is not defined

Así que decidí implementar mi propia clase Promise para ES3 a los compiladores js a continuación si no está definido. Simplemente agregue este código antes de su código principal y luego use Promise con seguridad.

if(typeof Promise === "undefined"){
    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) { 
            throw new TypeError("Cannot call a class as a function"); 
        }
    }
    var Promise = function () {
        function Promise(main) {
            var _this = this;
            _classCallCheck(this, Promise);
            this.value = undefined;
            this.callbacks = [];
            var resolve = function resolve(resolveValue) {
                _this.value = resolveValue;
                _this.triggerCallbacks();
            };
            var reject = function reject(rejectValue) {
                _this.value = rejectValue;
                _this.triggerCallbacks();
            };
            main(resolve, reject);
        }
        Promise.prototype.then = function then(cb) {
            var _this2 = this;
            var next = new Promise(function (resolve) {
                _this2.callbacks.push(function (x) {
                    return resolve(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.catch = function catch_(cb) {
            var _this2 = this;
            var next = new Promise(function (reject) {
                _this2.callbacks.push(function (x) {
                    return reject(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.triggerCallbacks = function triggerCallbacks() {
            var _this3 = this;
            this.callbacks.forEach(function (cb) {
                cb(_this3.value);
            });
        };
        return Promise;
    }();
}

28

Por supuesto, hay muchos enfoques, como solicitud síncrona, promesa, pero desde mi experiencia creo que debería usar el enfoque de devolución de llamada. Es natural el comportamiento asincrónico de Javascript. Por lo tanto, su fragmento de código puede reescribirse un poco diferente:

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            myCallback(response);
        }
    });

    return result;
}

function myCallback(response) {
    // Does something.
}

55
No hay nada inherentemente asíncrono sobre devoluciones de llamada o JavaScript.
Aluan Haddad

19

En lugar de arrojarle código, hay 2 conceptos que son clave para comprender cómo JS maneja las devoluciones de llamadas y la asincronía. (¿es eso una palabra?)

El modelo de bucle de eventos y concurrencia

Hay tres cosas que debe tener en cuenta; La cola; el bucle de eventos y la pila

En términos amplios y simplistas, el bucle de eventos es como el administrador del proyecto, está constantemente escuchando cualquier función que quiera ejecutarse y se comunica entre la cola y la pila.

while (queue.waitForMessage()) {
   queue.processNextMessage();
}

Una vez que recibe un mensaje para ejecutar algo, lo agrega a la cola. La cola es la lista de cosas que están esperando para ejecutarse (como su solicitud AJAX). imagínalo así:

 1. call foo.com/api/bar using foobarFunc
 2. Go perform an infinite loop
 ... and so on

Cuando uno de estos mensajes se va a ejecutar, saca el mensaje de la cola y crea una pila, la pila es todo lo que JS necesita ejecutar para realizar la instrucción en el mensaje. Entonces en nuestro ejemplo se le dice que llamefoobarFunc

function foobarFunc (var) {
  console.log(anotherFunction(var));
}

Entonces, cualquier cosa que foobarFunc necesite ejecutar (en nuestro caso anotherFunction) será empujada a la pila. ejecutado y luego olvidado: el bucle de eventos pasará al siguiente elemento de la cola (o escuchará mensajes)

La clave aquí es el orden de ejecución. Es decir

CUANDO va a correr algo

Cuando realiza una llamada con AJAX a una parte externa o ejecuta un código asincrónico (un setTimeout, por ejemplo), Javascript depende de una respuesta antes de que pueda continuar.

La gran pregunta es ¿cuándo obtendrá la respuesta? La respuesta es que no lo sabemos, por lo que el bucle de eventos está esperando que ese mensaje diga "oye, corre conmigo". Si JS solo esperara ese mensaje sincrónicamente, su aplicación se congelaría y apestaría. Entonces JS continúa ejecutando el siguiente elemento en la cola mientras espera que el mensaje se agregue nuevamente a la cola.

Es por eso que con la funcionalidad asincrónica usamos cosas llamadas devoluciones de llamada . Es como una promesa literalmente. Como en Prometo devolver algo en algún momento, jQuery usa devoluciones de llamada específicas llamadas deffered.done deffered.faily deffered.always(entre otras). Puedes verlos todos aquí

Entonces, lo que debe hacer es pasar una función que se promete ejecutar en algún momento con los datos que se le pasan.

Debido a que una devolución de llamada no se ejecuta de inmediato, pero más adelante es importante pasar la referencia a la función que no se ejecutó. entonces

function foo(bla) {
  console.log(bla)
}

así que la mayoría de las veces (pero no siempre), pasará por foonofoo()

Esperemos que tenga sentido. Cuando encuentre cosas como esta que parecen confusas, le recomiendo leer la documentación completa para al menos comprenderla. Te convertirá en un desarrollador mucho mejor.


18

Usando ES2017 debería tener esto como la declaración de función

async function foo() {
    var response = await $.ajax({url: '...'})
    return response;
}

Y ejecutándolo así.

(async function() {
    try {
        var result = await foo()
        console.log(result)
    } catch (e) {}
})()

O la sintaxis de Promise

foo().then(response => {
    console.log(response)

}).catch(error => {
    console.log(error)

})

¿podría esa segunda función ser reutilizable?
Zum Dummi

¿Cómo se usan los resultados si se llama oncolse, log? ¿No todo va a la consola en ese momento?
Ken Ingram
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.