¿Cómo devolver una respuesta de una llamada asíncrona?

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

Intenté devolver un valor de la devolución de llamada correcta y también asignar la respuesta a una variable local dentro de la función y devolverla, pero ninguno de estos métodos devuelve una 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`. 
4687
08 янв. Felix Kling establece el 08 de enero 2013-01-08 20:06 '13 a las 20:06 2013-01-08 20:06
@ 39 respuestas
  • 1
  • 2

→ Para obtener una explicación más general del comportamiento asíncrono en diferentes ejemplos, consulte. ¿Por qué mi variable no cambia después de cambiarla dentro de una función? - Enlace asíncrono al código.

→ Si ya ha comprendido el problema, vaya a las posibles soluciones a continuación.

Este problema

A en Ajax significa asíncrono . Esto significa que el envío de una solicitud (o, más bien, la recepción de una respuesta) se elimina del flujo normal de ejecución. En su ejemplo, $.ajax devuelve inmediatamente, y el próximo resultado de return result; declaración return result; , se ejecuta antes de que incluso se haya llamado a la función que usted pasó como devolución de llamada success .

Aquí hay una analogía que, con suerte, aclara la diferencia entre una transmisión síncrona y asíncrona:

síncrono

Imagina que llamas a un amigo y le pides que encuentre algo para ti. Aunque puede tardar un tiempo, esperas en el teléfono y miras al espacio hasta que tu amigo te da la respuesta que necesitas.

Lo mismo sucede cuando haces una llamada a una función que contiene un código "normal":

 function findItem() { var item; while(item_not_found) { // search } return item; } var item = findItem(); // Do something with item doSomethingElse(); 

Incluso si puede llevar mucho tiempo ejecutar findItem , cualquier código que siga a var item = findItem(); debe esperar hasta que la función devuelva un resultado.

Asíncrono

Llamas a tu amigo otra vez por la misma razón. Pero esta vez le dices que tienes prisa y que debería devolverte la llamada desde tu teléfono móvil. Cuelgas, sales de casa y haces todo lo que habías planeado. Tan pronto como tu amigo te devuelva la llamada, tratarás con la información que te dio.

Esto es exactamente lo que sucede cuando realiza una solicitud Ajax.

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

En lugar de esperar una respuesta, la ejecución continúa inmediatamente y la instrucción se ejecuta después de una llamada Ajax. Para recibir finalmente una respuesta, proporciona una función a la que se llamará después de recibir la respuesta, una devolución de llamada (tenga en cuenta algo, "devolución de llamada"). Cualquier declaración que sigue a esta llamada se ejecuta antes de la llamada de devolución de llamada.


Solución (s)

¡Adopta la naturaleza asíncrona de JavaScript! Aunque algunas operaciones asíncronas proporcionan contrapartes síncronas (como "Ajax"), generalmente no se recomiendan para su uso, especialmente en el contexto del navegador.

¿Por qué esto es malo, preguntas?

JavaScript se ejecuta en el subproceso de la IU del navegador, y cualquier proceso prolongado bloquea la interfaz de usuario, lo que hace que no responda. Además, hay un límite de tiempo de ejecución superior para JavaScript, y el navegador le preguntará al usuario si debe continuar la ejecución o no.

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

A continuación, vemos tres soluciones diferentes que se construyen una encima de la otra:

  • Promesas con async/await await (ES2017 +, disponible en navegadores más antiguos si utiliza un transportador o un regenerador)
  • Devolución de llamadas (popular en el sitio)
  • Promesas con then() (ES2015 +, disponible en navegadores más antiguos si utiliza una de las muchas bibliotecas de promesas)

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


ES2017 +: promete con async/await esperando

La versión ECMAScript lanzada en 2017 introdujo el soporte de sintaxis para funciones asíncronas. Con async y en await puede escribir de forma asíncrona en "estilo síncrono". El código sigue siendo asíncrono, pero más fácil de leer / entender.

async/await basa en promesas: la función async siempre devuelve una promesa. await "desenvuelve" la promesa y, o bien el resultado en el significado de la promesa se resolvió con o da un error si la promesa fue rechazada.

Importante: Solo puede usar await dentro de una función async . En este momento, en el nivel más alto, await todavía no es compatible, por lo que es posible que tenga que hacer que el IIFE asíncrono inicie el contexto async .

Puede leer más sobre async y await en 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); })(); 

Las versiones actuales del navegador y del host son compatibles con async/await . También puede mantener entornos más antiguos convirtiendo su código a ES5 usando un regenerador (o herramientas usando un regenerador, como Babel ).


Deje que las funciones acepten devoluciones de llamada.

Una devolución de llamada es simplemente una función pasada a otra función. Esta otra función puede llamar a una función pasada siempre que esté lista. En el contexto de un proceso asíncrono, se llamará una devolución de llamada siempre que se ejecute un proceso asíncrono. Normalmente el resultado se envía a la devolución de llamada.

En el ejemplo de pregunta, puede forzar a foo a aceptar una devolución de llamada y usarla como devolución de llamada success . Entonces esto

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

se convierte en

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

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

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

foo sí se define de la siguiente manera:

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

callback se referirá a la función que pasamos a foo cuando la llamamos, y simplemente la pasamos al success . Es decir tan pronto como la solicitud de Ajax tenga éxito, $.ajax invocará la callback y $.ajax respuesta a la devolución de llamada (a la que se puede hacer referencia usando el result , ya que así es como definimos la devolución de llamada).

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

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

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


ES2015 +: promete con entonces ()

Promise API es una nueva característica de ECMAScript 6 (ES2015), pero ya tiene un buen soporte de navegador . También hay muchas bibliotecas que implementan las Promesas API estándar y proporcionan métodos adicionales para simplificar el uso y la composición de las funciones asíncronas (por ejemplo, bluebird ).

Las promesas son contenedores de valores futuros. Cuando una promesa recibe un valor (está permitido) 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 separar su código y es más fácil componerlas.

Aquí hay un ejemplo simple de usar promesas:

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

Con respecto a nuestra llamada Ajax, podríamos usar las siguientes promesas:

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

Una descripción de todos los beneficios que prometen ofrecer está más allá del alcance de esta respuesta, pero si está escribiendo un nuevo código, debe considerarlos seriamente. Proporcionan una excelente abstracción y separación de su código.

Más información sobre promesas: HTML5 - rocas - promesas de JavaScript

Nota: jQuery objetos pendientes

Los objetos diferidos son una implementación personalizada de promesas en jQuery (antes de la estandarización de la API de Promesa). Casi se comportan como promesas, pero exponen una API ligeramente diferente.

Cada método jQuery Ajax ya devuelve un "objeto pendiente" (en realidad una promesa de un objeto pendiente), que simplemente puede devolver desde su función:

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

Nota: la promesa resultó

Recuerde que las promesas y los objetos diferidos son solo contenedores para el valor futuro, no el valor en sí. Por ejemplo, supongamos que tienes 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 malinterpreta los problemas asíncronos anteriores. En particular, $.ajax() no congela el código mientras comprueba la página '/ password' en su servidor: envía una solicitud al servidor y, mientras espera, devuelve inmediatamente el objeto jQuery Ajax Deferred, no la respuesta del servidor. Esto significa que la if siempre recibirá este objeto diferido, lo procesará como true y actuará como si el usuario hubiera iniciado sesión. No es bueno

Pero arreglarlo fácilmente:

 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 dije, algunas (!) Operaciones asíncronas tienen contrapartes síncronas. No abogo por su uso, pero en aras de la integridad, a continuación le indicamos cómo debe realizar una llamada sincrónica:

No jQuery

Si está utilizando directamente el objeto XMLHTTPRequest , pase false como tercer argumento .open .

Jquery

Si usa jQuery , puede establecer el parámetro async en false . Tenga en cuenta que esta opción está en desuso desde jQuery 1.8. Entonces aún puede utilizar la devolución de llamada correcta o el acceso a la propiedad responseText del objeto jqXHR :

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

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

Cuidado No se puede hacer una solicitud JSONP síncrona. JSONP es siempre de naturaleza asíncrona (otra razón para no considerar esta opción).

5011
08 янв. La respuesta la da Felix Kling el 8 de enero. 2013-01-08 20:06 '13 a las 20:06 2013-01-08 20:06

Si no usa jQuery en su código, esta respuesta es para usted.

Su código debe ser algo como 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 gran trabajo al escribir la respuesta para las personas que usan jQuery para AJAX, decidí ofrecer una alternativa para las personas que no lo hacen.

( Tenga en cuenta que para aquellos que utilizan la nueva fetch API, Angular o promesas, agregué otra respuesta a continuación )


Que te encuentras

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

A en AJAX significa asíncrono . Esto significa que el envío de una solicitud (o, más bien, la recepción de una respuesta) se elimina del flujo normal de ejecución. En su ejemplo, .send devuelve de inmediato, y el próximo resultado de la declaración return result; se ejecuta antes de que incluso se haya llamado a la función que pasó como devolución de llamada success .

Esto significa que cuando regresa, el oyente que especificó aún no se ha cumplido, lo que significa que no se determinó el valor que devolvió.

Analogía simple

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

(Feeddle)

El valor de retorno de a undefined está undefined , ya que la parte a=5 aún no se ha ejecutado. AJAX hace esto: devuelve el valor antes de que el servidor pudiera decirle a su navegador cuál es el valor.

Una posible solución a este problema es reutilizar el código, informando a su programa sobre 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, le getFive acción que debe realizarse cuando se completa, le decimos a nuestro código cómo reaccionar cuando finaliza el evento (por ejemplo, nuestra llamada AJAX o, en este caso, un tiempo de espera).

Uso:

 getFive(onComplete); 

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

Posibles soluciones

border=0

Hay básicamente dos formas de resolver este problema:

  • Haz una llamada AJAX síncrona (llámalo SJAX).
  • Reestructura tu código para que funcione correctamente con devoluciones de llamada.

1. AJAX síncrono - ¡No lo hagas!

En cuanto al AJAX síncrono, no lo hagas! La respuesta de Félix da algunos argumentos sólidos de por qué esto es una mala idea. Para resumir, congelará el navegador del usuario hasta que el servidor devuelva una respuesta y cree una interfaz de usuario muy mala. Aquí hay otro resumen de MDN de por qué:

XMLHttpRequest admite comunicaciones tanto síncronas como asíncronas. En general, sin embargo, las solicitudes asíncronas deberían ser preferibles a las solicitudes sincrónicas por razones de rendimiento.

En resumen, las solicitudes síncronas bloquean la ejecución del código ...... esto puede causar serios problemas ...

Si necesita hacer esto, puede pasar la bandera: A continuación le indicamos cómo:

 var request = new XMLHttpRequest(); request.open('GET', 'yourURL', false); // `false` makes the request synchronous request.send(null); if (request.status === 200) {// That HTTP for 'ok' console.log(request.responseText); } 

2. Código de Reestructuración.

Deja que tu función acepte la devolución de llamada. En el ejemplo, se puede hacer que el código foo acepte una devolución de llamada. Le diremos a nuestro código cómo reaccionar cuando foo termine.

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 solo podríamos pasar un enlace a una función existente, haciéndolo así:

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

Para obtener más información sobre cómo se ejecuta este tipo de devolución de llamada, verifique 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 nuestra función foo acepta una acción que comienza cuando AJAX se completa con éxito, podemos continuar verificando si el estado de respuesta 200 está respondiendo y actúa en consecuencia (crear un controlador de errores, etc.). Una solución efectiva a nuestro problema.

Si aún tiene problemas para entender esto, lea la Guía de introducción de AJAX en MDN.

953
30 мая '13 в 2:30 2013-05-30 02:30 Respuesta dada por Benjamin Gruenbaum 30 de mayo, '13 a las 2:30 2013-05-30 02:30

XMLHttpRequest 2 (Primero lea las respuestas de Benjamin Grünbaum y Felix Kling )

Si no está utilizando jQuery y desea obtener un XMLHttpRequest 2 corto y agradable que funciona en los navegadores modernos, así como en los navegadores móviles, sugiero usarlo de la siguiente 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 tanto, no hay cierres innecesarios innecesarios).
  3. Hay algunas otras situaciones que no recuerdo que hacen que XMLHttpRequest 1 sea molesto.

Hay dos formas de obtener una respuesta a esta llamada Ajax (tres usando el nombre de variable XMLHttpRequest):

El más simple:

 this.response 

O, si por alguna razón usted bind() devolución de llamada con una clase:

 e.target.response 

Ejemplo:

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

O (las mejores funciones anónimas anteriores son siempre un problema):

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

No hay nada más fácil.

Ahora, algunas personas probablemente dirán que es mejor usar onreadystatechange o incluso el nombre de la variable XMLHttpRequest. Esto es incorrecto

Explore las características avanzadas de XMLHttpRequest

Todos los navegadores modernos son compatibles. Y puedo confirmar que utilizo este enfoque porque hay XMLHttpRequest 2. Nunca he tenido ningún problema en todos los navegadores que uso.

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

Usar el nombre de la variable XMLHttpRequest es otro gran error, ya que necesita volver a llamar dentro de los cierres de onload / oreadystatange, de lo contrario, lo habrá perdido.


Ahora, si quieres algo más complejo usando Post y FormData, puedes extender esta función fácilmente:

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

Nuevamente ... esta es una función muy corta, pero recibe y publica.

Ejemplos de uso:

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

O pase el elemento de formulario completo ( document.getElementsByTagName('form')[0] ):

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

O establece algunos valores personalizados:

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

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

Dicho esto ... ¿por qué no hacerlo de una manera sencilla?


Como se mencionó en el comentario, el uso de error sincrónico viola completamente el significado de la respuesta. ¿Cuál es una buena forma corta de usar Ajax correctamente?

Manejador 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á definido estáticamente, por lo que no compromete la función. El controlador de errores se puede utilizar para otras funciones.

Pero para realmente obtener el error, la única forma es escribir la URL incorrecta, en cuyo caso cada navegador da un error.

Los controladores de errores pueden ser útiles si establece encabezados personalizados, establece responseType para un búfer de matriz de blob o algo más ...