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`.
→ Para una explicación más general del comportamiento asíncrono con diferentes ejemplos, por favor vea https://stackoverflow.com/q/23667086/218196
*→ Si ya entiendes el problema, salta a las posibles soluciones de abajo.
El problema
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,
$.ajax
retorna inmediatamente y la siguiente sentencia,return result;
, se ejecuta antes de que la función que pasaste como callbacksuccess
haya sido llamada. Aquí hay una analogía que espero que aclare la diferencia entre el flujo síncrono y el asíncrono:Sincrónico
Imagina que llamas por teléfono a un amigo y le pides que te busque algo. Aunque puede tardar un rato, esperas al teléfono y miras fijamente al espacio, hasta que tu amigo te da la respuesta que necesitabas. Lo mismo ocurre cuando haces una llamada a una función que contiene código "normal":
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Aunque findItem
pueda tardar mucho en ejecutarse, cualquier código que venga después de var item = findItem();
tiene que esperar hasta que la función devuelva el resultado.
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.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
**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)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+.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
y await
, 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ón async
siempre devuelve una promesa. La función await
"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 usar await
dentro de una función async
. En este momento, el nivel superior de await
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 contexto async
.
Puedes 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);
})();
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).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 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 "inline" pero se puede pasar cualquier referencia a la función:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
El propio foo
se define de la siguiente manera:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
se referirá a la función que le pasamos a foo
cuando la llamamos y simplemente se la pasamos a success
. Es decir, una vez que la petición Ajax tiene éxito, $.ajax
llamará a callback
y pasará la respuesta a la callback (a la que se puede referir con resultado
, ya que así es como definimos la callback).
También se puede procesar la respuesta antes de pasarla al callback:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
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:
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 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
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:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
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
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 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 sentencia if
siempre va a obtener este objeto Deferred, lo tratará como true
, y procederá como si el usuario estuviera conectado. Esto 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
});
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:
Si utiliza directamente un objeto XMLHTTPRequest
, pase false
como tercer argumento a .open
.
Si utiliza jQuery, puede establecer la opción async
a false
. Tenga en cuenta que esta opción está desaparecida desde jQuery 1.8.
En ese caso, puede seguir utilizando una llamada de retorno success
o acceder a la propiedad responseText
del objeto jqXHR:
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
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).
Tu código debería ser algo parecido 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 los que usan la nueva API fetch
, Angular o promises he añadido otra respuesta más abajo)
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 callback success
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
function getFive(){
var a;
setTimeout(function(){
a=5;
},10);
return a;
}
[(Fiddle)][2]
El valor de a
devuelto es undefined
ya que la parte a=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.
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 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:
getFive(onComplete);
Lo que debería alertar "5" a la pantalla. [(Fiddle)][4].
Hay básicamente dos formas de resolver esto:
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é:
XMLHttpRequest soporta tanto comunicaciones síncronas como asíncronas. En general, sin embargo, las peticiones asíncronas deberían ser preferidas a las síncronas por razones de rendimiento.
En resumen, las peticiones síncronas bloquean la ejecución del código... ...esto puede causar serios problemas...
Si usted tiene que hacerlo, puede pasar una bandera: Aquí está cómo:
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);
}
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 cuando foo
se complete.
Así que:
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 como
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
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
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();
}
[(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:
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:
this.response
O si por alguna razón bind()
el callback a una clase:
e.target.response
Ejemplo:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
O (la anterior es mejor las funciones anónimas son siempre un problema):
ajax('URL', function(e){console.log(this.response)});
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:
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)
}
De nuevo... es una función muy corta, pero consigue & post. 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 formulario completo (document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
O establecer algunos valores personalizados:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
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
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);
displayAjax()
bajo this.statusText
como Method 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á el this.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 callback displayAjax()
.
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.
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');
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).