→ 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, $.ajax
devuelve inmediatamente y la siguiente instrucción, return result;
se ejecuta antes de que success
incluso 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 findItem
puede 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 con
async/await
(ES2017 +, disponible en navegadores antiguos si usa un transpilador o regenerador)
- Callbacks (popular en nodo)
- Promesas con
then()
(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 async
y 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/await
se basa en promesas: una async
funció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 await
dentro de una async
función. En este momento, el nivel superior await
aú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 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);
})();
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 foo
acepte una devolución de llamada y usarla como success
devolució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
});
}
callback
se referirá a la función a la que pasamos foo
cuando la llamamos y simplemente la pasamos a success
. Es decir, una vez que la solicitud de Ajax sea exitosa, $.ajax
llamará callback
y 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 if
declaración siempre obtendrá este objeto diferido, lo tratará como true
y 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 XMLHTTPRequest
objeto, pase false
como tercer argumento a .open
.
jQuery
Si usa jQuery , puede establecer la async
opción en false
. Tenga en cuenta que esta opción está en desuso desde jQuery 1.8. A continuación, puede seguir utilizando una success
devolución de llamada o acceder a la responseText
propiedad 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 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).