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

editado octubre 2019 en Javascript

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

Intenté devolver el valor de la devolución de success callback, 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

Funciones: asynchronous y xmlhttprequest

Adjunto aquí el detalle de la función foo


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`.


Comentarios

  • 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 regresa inmediatamente y la siguiente instrucción, return result ;, se ejecuta antes de que se llamara a la función que pasó como devolución de llamada exitosa.

    Aquí hay una analogía que, con suerte, hace más clara la diferencia entre flujo sincrónico y asincrónico:

    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 tardar mucho tiempo en ejecutarse, cualquier código que venga después de 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 eventualmente, usted proporciona una función a la que se llamará una vez que se recibió la respuesta, una devolución de llamada (¿nota algo? ¿Devolver 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 que preguntas?

    JavaScript se ejecuta en el subproceso de interfaz de usuario del navegador y cualquier proceso de ejecución prolongada bloqueará la interfaz de usuario, por lo que no responderá. Además, hay un límite superior en el tiempo de ejecución de JavaScript y el navegador le preguntará al usuario si 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:

    • Promesas 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+.



  • Te dejo aquí las diferentes soluciones:

    ES2017 +: Promesas con asíncrono / espera

    La versión ECMAScript lanzada en 2017 introdujo soporte de nivel de sintaxis para funciones asincrónicas. Con la ayuda de async y wait, puede escribir asincrónico 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 función asincrónica siempre devuelve una promesa. esperar "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 wait dentro de una función asíncrona. En este momento, la espera de nivel superior aún no es compatible, por lo que es posible que deba realizar una IIFE asincrónica (expresión de función invocada inmediatamente) para iniciar un contexto asincrónico.

    Puede leer más sobre async y esperar 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 de navegador y nodo admiten async / 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 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 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 callback y usarla como callback exitosa. 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 puede pasar cualquier referencia de función:

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

    foo 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 a success. Es decir. una vez que la solicitud de Ajax es exitosa, $ .ajax llamará a la callback y pasará la respuesta a la callback (a la que se puede hacer referencia con el resultado, ya que así es como definimos la devolución de llamada).

    También puede procesar la respuesta antes de pasarla a la callback:

    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 simplemente pensando en el flujo de la aplicación.


  • ES2015 +: Promesas con then()

    La API Promises es una nueva característica de ECMAScript 6 (ES2015), pero ya tiene un buen soporte de navegador. 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 callbacks 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.

    Cada método Ajax de jQuery ya devuelve 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 revisa la página '/ contraseña' en su servidor: envía una solicitud al servidor y mientras espera, devuelve inmediatamente un objeto jQuery Ajax diferido, no la respuesta de el servidor. Eso significa que la declaración "if" siempre obtendrá este objeto diferido, trátelo como "verdadero" y proceda 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 objeto "XMLHTTPRequest", pase "false" como tercer argumento a .open.


    jQuery

    Si usa jQuery, puede establecer la opción "asíncrono" en "falso". Tenga en cuenta que esta opción está en desuso desde jQuery 1.8. Entonces puede usar una "callback exitosa" o acceder 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).

    ¡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).

Accede o Regístrate para comentar.