¿Cómo devuelvo la respuesta de una llamada asíncrona?
Tengo una función foo
que hace una petición Ajax. Cómo puedo devolver la respuesta de foo
?
He intentado devolver el valor de la llamada de retorno success
así como asignar la respuesta a una variable local dentro de la función y devolverla, pero ninguna de esas formas devuelve realmente 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`.
5183
3
Aunque
findItem
pueda tardar mucho en ejecutarse, cualquier código que venga después devar item = findItem();
tiene que esperar hasta que la función devuelva el resultado.Asíncrono
Vuelves a llamar a tu amigo por el mismo motivo. Pero esta vez le dices que tienes prisa y que te llame al móvil. Cuelgas, sales de casa y haces lo que tenías pensado hacer. Una vez que tu amigo te devuelve la llamada, te ocupas de la información que te ha dado. Eso es exactamente lo que ocurre cuando haces una petición Ajax.
En lugar de esperar la respuesta, la ejecución continúa inmediatamente y se ejecuta la declaración después de la llamada Ajax. Para obtener la respuesta eventualmente, se proporciona una función para ser llamada una vez que la respuesta fue recibida, un callback (¿notas algo? call back ?). Cualquier sentencia que venga después de esa llamada se ejecuta antes de llamar al callback.
Solución(es)
**Mientras que ciertas operaciones asíncronas proporcionan contrapartes síncronas (también lo hace "Ajax"), generalmente se desaconseja su uso, especialmente en el contexto del navegador. ¿Por qué es malo? 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, haciendo que no responda. Además, hay un límite máximo en el tiempo de ejecución de JavaScript y el navegador preguntará al usuario si desea continuar la ejecución o no. Todo esto es realmente una mala experiencia para el usuario. 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 superponen entre sí:
async/await
(ES2017+, disponible en navegadores antiguos si se utiliza un transpilador o regenerador)Promesas con
then()
(ES2015+, disponible en navegadores antiguos si se utiliza una de las muchas bibliotecas de promesas) Los tres están disponibles en los navegadores actuales, y en node 7+.ES2017+: Promesas con
async/await
La versión de ECMAScript lanzada en 2017 introdujo soporte a nivel de sintaxis para funciones asíncronas. Con la ayuda de
async
yawait
, se puede escribir asíncrono en un "estilo síncrono". El código sigue siendo asíncrono, pero es más fácil de leer/entender.async/await
se basa en las promesas: una funciónasync
siempre devuelve una promesa. La funciónawait
"desenvuelve" una promesa y da como resultado el valor con el que se resolvió la promesa o lanza un error si la promesa fue rechazada. Importante: Sólo puedes usarawait
dentro de una funciónasync
. En este momento, el nivel superior deawait
aún no está soportado, por lo que es posible que tengas que hacer un IIFE asíncrono (Immediately Invoked Function Expression) para iniciar un contextoasync
. Puedes leer más sobreasync
yawait
en MDN. Aquí hay un ejemplo que se basa en el retraso anterior:Las versiones actuales de browser y node soportan
async/await
. También puedes soportar entornos más antiguos transformando tu código a ES5 con la ayuda de regenerator (o herramientas que usan regenerator, como Babel).Dejar que las funciones acepten callbacks
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 asíncrono, el callback será llamado cuando el proceso asíncrono haya terminado. Normalmente, el resultado se pasa a la llamada de retorno. En el ejemplo de la pregunta, puedes hacer que
foo
acepte una llamada de retorno y usarla como llamada de retorno de éxito. Así que estose convierte en
Aquí definimos la función "inline" pero se puede pasar cualquier referencia a la función:
El propio
foo
se define de la siguiente manera:callback
se referirá a la función que le pasamos afoo
cuando la llamamos y simplemente se la pasamos asuccess
. Es decir, una vez que la petición Ajax tiene éxito,$.ajax
llamará acallback
y pasará la respuesta a la callback (a la que se puede referir conresultado
, ya que así es como definimos la callback). También se puede procesar la respuesta antes de pasarla al callback:Es más fácil de lo que parece escribir código utilizando callbacks. Después de todo, JavaScript en el navegador está fuertemente orientado a eventos (eventos DOM). Recibir la respuesta Ajax no es más que un evento.
Podrían surgir dificultades cuando tengas que trabajar con código de terceros, pero la mayoría de los problemas se pueden resolver simplemente pensando en el flujo de la aplicación.
ES2015+: Promesas con then()
La API de promesas es una nueva característica de ECMAScript 6 (ES2015), pero ya tiene un buen soporte por parte de los navegadores. 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 asíncronas (por ejemplo, bluebird). Las promesas son contenedores de valores futuros. Cuando la promesa recibe el valor (es resuelta) o cuando es cancelada (rechazada), notifica a todos sus "oyentes" que quieren acceder a este valor. La ventaja sobre los callbacks simples es que permiten desacoplar el código y son más fáciles de componer. Este es un ejemplo sencillo de uso de una promesa:
Aplicado a nuestra llamada Ajax podríamos usar promesas como esta
Describir todas las ventajas que ofrecen las promesas está más allá del alcance de esta respuesta, pero si escribes código nuevo, deberías considerarlas seriamente. Proporcionan una gran abstracción y separación de tu código. Más información sobre las promesas: HTML5 rocks - JavaScript Promises
Nota al margen: los objetos diferidos de jQuery
Los Deferred objects son la implementación personalizada de jQuery de las promesas (antes de que se estandarizara la API de promesas). Se comportan casi como las promesas pero exponen una API ligeramente diferente. Cada método Ajax de jQuery ya devuelve un "objeto diferido" (en realidad una promesa de un objeto diferido) que puedes simplemente devolver desde tu función:
Nota al margen: Promise gotchas
Tenga en cuenta que las promesas y los objetos diferidos son sólo contenedores para un valor futuro, no son el valor en sí. Por ejemplo, supongamos que tenemos lo siguiente
Este código no entiende los problemas de asincronía anteriores. Específicamente,
$.ajax()
no congela el código mientras comprueba la página '/password' en su servidor - envía una petición al servidor y mientras espera, devuelve inmediatamente un objeto jQuery Ajax Deferred, no la respuesta del servidor. Esto significa que la sentenciaif
siempre va a obtener este objeto Deferred, lo tratará comotrue
, y procederá como si el usuario estuviera conectado. Esto no es bueno. Pero la solución es fácil:No se recomienda: Llamadas "Ajax" sincrónicas
Como he mencionado, algunas operaciones asíncronas tienen contrapartidas síncronas. No abogo por su uso, pero por si acaso, así es como se realizaría una llamada sincrónica:
Sin jQuery
Si utiliza directamente un objeto
XMLHTTPRequest
, pasefalse
como tercer argumento a.open
.jQuery
Si utiliza jQuery, puede establecer la opción
async
afalse
. Tenga en cuenta que esta opción está desaparecida desde jQuery 1.8. En ese caso, puede seguir utilizando una llamada de retornosuccess
o acceder a la propiedadresponseText
del objeto jqXHR:Si utilizas cualquier otro método jQuery Ajax, como
$.get
,$.getJSON
, etc., tienes que cambiarlo por$.ajax
(ya que sólo puedes pasar parámetros de configuración a$.ajax
). **No es posible hacer una petición síncrona JSONP. JSONP, por su propia naturaleza, es siempre asíncrono (una razón más para no considerar esta opción).Si no estás usando jQuery en tu código, esta respuesta es para ti
Tu código debería ser algo parecido a esto:
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 los que usan la nueva API
fetch
, Angular o promises he añadido otra respuesta más abajo)Lo que se está enfrentando
Este es un breve resumen de la "Explicación del problema" de la otra respuesta, si no estás seguro después de leer esto, lee eso.
La A en AJAX significa asíncrono. Eso significa que el envío de la petición (o más bien la recepción de la respuesta) se saca del flujo de ejecución normal. En tu ejemplo,
.send
devuelve inmediatamente y la siguiente sentencia,return result;
, se ejecuta antes de que la función que pasaste como callbacksuccess
haya sido llamada.Esto significa que cuando está regresando, el listener que has definido no se ha ejecutado todavía, lo que significa que el valor que estás devolviendo no ha sido definido.
He aquí una simple analogía
[(Fiddle)][2]
El valor de
a
devuelto esundefined
ya que la partea=5
no se ha ejecutado todavía. AJAX actúa así, está devolviendo el valor antes de que el servidor tenga la oportunidad de decirle al navegador cuál es ese valor.Una posible solución a este problema es codificar re-activamente , diciéndole a tu programa qué hacer cuando el cálculo se haya completado.
Esto se llama CPS. Básicamente, estamos pasando a
getFive
una acción a realizar cuando se completa, estamos diciendo a nuestro código cómo reaccionar cuando un evento se completa (como nuestra llamada AJAX, o en este caso el tiempo de espera).El uso sería:
Lo que debería alertar "5" a la pantalla. [(Fiddle)][4].
Posibles soluciones
Hay básicamente dos formas de resolver esto:
1. AJAX sincrónico - ¡No lo hagas!
En cuanto a AJAX sincrónico, 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 creará una muy mala experiencia de usuario. Aquí hay otro breve resumen tomado de MDN sobre el porqué:
Si usted tiene que hacerlo, puede pasar una bandera: Aquí está cómo:
2. Reestructurar el código
Deje que su función acepte una devolución de llamada. En el código de ejemplo se puede hacer que
foo
acepte una llamada de retorno. Le diremos a nuestro código cómo reaccionar cuandofoo
se complete.Así que:
Se convierte en:
Aquí pasamos una función anónima, pero podríamos pasar fácilmente una referencia a una función existente, haciendo que se vea como
Para más detalles sobre cómo se hace este tipo de diseño de devolución de llamada, revisa la respuesta de Felix.
Ahora, definamos el propio foo para que actúe en consecuencia
[(fiddle)][6]
Ahora hemos hecho que nuestra función foo acepte una acción que se ejecute cuando el AJAX se complete con éxito, podemos extender esto más allá comprobando si el estado de la respuesta no es 200 y actuando en consecuencia (crear un manejador de fallos y tal). Resolviendo efectivamente nuestro problema.
Si todavía te cuesta entender esto lee la guía de inicio de AJAX en MDN.
XMLHttpRequest 2 (primero lee las respuestas de Benjamin Gruenbaum y Felix Kling) Si no usas jQuery y quieres un XMLHttpRequest 2 bonito y corto que funcione en los navegadores modernos y también en los móviles te sugiero que lo uses de esta manera:
Como puedes ver:
O si por alguna razón
bind()
el callback a una clase:Ejemplo:
O (la anterior es mejor las funciones anónimas son siempre un problema):
Nada más fácil. Ahora algunas personas probablemente dirán que es mejor usar onreadystatechange o incluso el nombre de la variable XMLHttpRequest. Eso es incorrecto. Mira XMLHttpRequest características avanzadas Soporta todos los *navegadores modernos. Y puedo confirmarlo ya que estoy usando este enfoque desde que existe XMLHttpRequest 2. Nunca he tenido ningún tipo de problema en todos los navegadores que uso. onreadystatechange solo es útil si quieres obtener las cabeceras en el estado 2. Usar el nombre de la variable
XMLHttpRequest
es otro gran error ya que necesitas ejecutar el callback dentro de los cierres de onload/oreadystatechange sino lo pierdes.Ahora bien, si quieres algo más complejo usando post y FormData puedes extender fácilmente esta función:
De nuevo... es una función muy corta, pero consigue & post. Ejemplos de uso:
O pasar un elemento de formulario completo (
document.getElementsByTagName('form')[0]
):O establecer algunos valores personalizados:
Como puedes ver no he implementado la sincronización... es algo malo. Dicho esto... ¿por qué no lo haces de la manera más fácil?
Como se menciona en el comentario el uso de error && síncrono hace romper completamente el punto de la respuesta. ¿Cuál es una forma corta de usar Ajax de la manera correcta? Error handler
En el script anterior, tienes un manejador de errores que está definido estáticamente para no comprometer la función. El manejador de errores puede ser usado para otras funciones también. Pero para realmente sacar un error la única manera es escribir una URL incorrecta en cuyo caso todos los navegadores arrojan un error. Los manejadores de error son tal vez útiles si se establecen cabeceras personalizadas, se establece el responseType a blob array buffer o lo que sea... Incluso si pasas 'POSTAPAPAP' como método, no arrojará un error. Incluso si pasas 'fdggdgilfdghfldj' como formdata no dará error. En el primer caso el error está dentro de
displayAjax()
bajothis.statusText
comoMethod not Allowed
. En el segundo caso, simplemente funciona. Tienes que comprobar en el lado del servidor si has pasado los datos correctos del post. cross-domain not allowed lanza un error automáticamente. En la respuesta de error, no hay códigos de error. Sólo está elthis.type
que se establece como error. ¿Por qué añadir un manejador de errores si no tienes ningún control sobre los errores? La mayoría de los errores son devueltos dentro de esto en la función callbackdisplayAjax()
. Así que: No es necesario comprobar los errores si eres capaz de copiar y pegar la URL correctamente. ;) PS: Como primera prueba escribí x('x', displayAjax)..., y totalmente obtuvo una respuesta...?? Así que comprobé la carpeta donde se encuentra el HTML, y había un archivo llamado 'x.xml'. Así que incluso si olvidas la extensión de tu archivo XMLHttpRequest 2 LO ENCONTRARÁ. Me ha hecho graciaLeer un archivo sincrónico *No hagas eso. Si quieres bloquear el navegador durante un rato carga un bonito y gran archivo
.txt
de forma sincrónica.Ahora puedes hacer
No hay otra forma de hacer esto de forma no asíncrona. (Sí, con el bucle setTimeout... ¿pero en serio?) Otro punto es... si trabajas con APIs o simplemente con los archivos de tu propia lista o lo que sea siempre usas funciones diferentes para cada petición... Sólo si tienes una página donde cargas siempre el mismo XML/JSON o lo que sea necesitas una sola función. En ese caso, modifica un poco la función Ajax y sustituye b por tu función especial.
Las funciones anteriores son para uso básico. Si quieres EXTENDER la función... Sí, puedes hacerlo. Yo uso muchas APIs y una de las primeras funciones que integro en cada página HTML es la primera función Ajax de esta respuesta, sólo con GET... Pero puedes hacer muchas cosas con XMLHttpRequest 2: Hice un gestor de descargas (usando rangos en ambos lados con resume, filereader, filesystem), varios conversores de imágenes usando canvas, poblar bases de datos web SQL con base64images y mucho más... Pero en estos casos deberías crear una función sólo para ese propósito... a veces necesitas un blob, array buffers, puedes establecer cabeceras, anular mimetype y hay mucho más... Pero la cuestión aquí es cómo devolver una respuesta Ajax... (He añadido una forma fácil).