Llamar a una función asincrónica de Javascript sincrónicamente

222

Primero, este es un caso muy específico de hacerlo de manera incorrecta a propósito para adaptar una llamada asincrónica a una base de código muy síncrona que tiene muchos miles de líneas de largo y el tiempo actualmente no permite la capacidad de hacer los cambios para "hacer bien." Me duele cada fibra de mi ser, pero la realidad y los ideales a menudo no encajan. Sé que esto apesta.

Bien, eso fuera del camino, ¿cómo lo hago para poder:

function doSomething() {

  var data;

  function callBack(d) {
    data = d;
  }

  myAsynchronousCall(param1, callBack);

  // block here and return data when the callback is finished
  return data;
}

Todos los ejemplos (o la falta de ellos) usan bibliotecas y / o compiladores, los cuales no son viables para esta solución. Necesito un ejemplo concreto de cómo hacer que se bloquee (por ejemplo, NO deje la función doSomething hasta que se llame la devolución de llamada) SIN congelar la IU. Si tal cosa es posible en JS.

Robert C. Barth
fuente
16
Simplemente no es posible bloquear el navegador y esperar. Simplemente no lo harán.
Puntiagudo
2
javascript no tiene mecanismos de bloqueo en la mayoría de los navegadores ... querrá crear una devolución de llamada que se llama cuando la llamada asincrónica termina para devolver los datos
Nadir Muzaffar
8
Estás pidiendo una forma de decirle al navegador "Sé que acabo de decirte que ejecutes esa función anterior de forma asincrónica, ¡pero no lo dije en serio!". ¿Por qué esperarías que eso sea posible?
Wayne
2
Gracias Dan por la edición. No era estrictamente grosero, pero tu redacción es mejor.
Robert C. Barth
2
@ RobertC.Barth Ahora también es posible con JavaScript. Las funciones de espera asíncrona aún no se han ratificado en el estándar, pero se planea que estén en ES2017. Vea mi respuesta a continuación para más detalles.
John

Respuestas:

135

"no me digas cómo debería hacerlo" de la manera correcta "o lo que sea"

OKAY. pero realmente deberías hacerlo de la manera correcta ... o lo que sea

"Necesito un ejemplo concreto de cómo hacer que se bloquee ... SIN congelar la interfaz de usuario. Si tal cosa es posible en JS".

No, es imposible bloquear el JavaScript en ejecución sin bloquear la interfaz de usuario.

Dada la falta de información, es difícil ofrecer una solución, pero una opción puede ser hacer que la función de llamada realice un sondeo para verificar una variable global y luego establecer la devolución de llamada dataen global.

function doSomething() {

      // callback sets the received data to a global var
  function callBack(d) {
      window.data = d;
  }
      // start the async
  myAsynchronousCall(param1, callBack);

}

  // start the function
doSomething();

  // make sure the global is clear
window.data = null

  // start polling at an interval until the data is found at the global
var intvl = setInterval(function() {
    if (window.data) { 
        clearInterval(intvl);
        console.log(data);
    }
}, 100);

Todo esto supone que puede modificar doSomething(). No sé si eso está en las cartas.

Si se puede modificar, entonces no sé por qué no pasarías una devolución de llamada para doSomething()que se llame desde la otra devolución de llamada, sino que mejor me detengo antes de meterme en problemas. ;)


Oh, qué diablos. Diste un ejemplo que sugiere que se puede hacer correctamente, así que voy a mostrar esa solución ...

function doSomething( func ) {

  function callBack(d) {
    func( d );
  }

  myAsynchronousCall(param1, callBack);

}

doSomething(function(data) {
    console.log(data);
});

Debido a que su ejemplo incluye una devolución de llamada que se pasa a la llamada asíncrona, la forma correcta sería pasar una función para doSomething()que se invoque desde la devolución de llamada.

Por supuesto, si eso es lo único que está haciendo la devolución de llamada, simplemente pasaría funcdirectamente ...

myAsynchronousCall(param1, func);
usuario1106925
fuente
22
Sí, sé cómo hacerlo correctamente, necesito saber cómo / si se puede hacer incorrectamente por la razón específica indicada. El quid es que no quiero dejar doSomething () hasta que myAsynchronousCall complete la llamada a la función de devolución de llamada. Bleh, no se puede hacer, como sospechaba, solo necesitaba la sabiduría recopilada de Internet para respaldarme. Gracias. :-)
Robert C. Barth
2
@ RobertC.Barth: Sí, desafortunadamente sus sospechas fueron correctas.
¿Soy yo o solo funciona la versión "hecha correctamente"? La pregunta incluye una llamada de vuelta, ante el cual, debe haber algo que espera la llamada asincrónica a fin, que esta primera parte de esta respuesta no cubre ...
ravemir
@ravemir: La respuesta dice que no es posible hacer lo que quiere. Esa es la parte importante para entender. En otras palabras, no puede hacer una llamada asincrónica y devolver un valor sin bloquear la IU. Entonces, la primera solución es un hack feo que usa una variable global y un sondeo para ver si esa variable ha sido modificada. La segunda versión es la forma correcta.
1
@Leonardo: Es la misteriosa función que se llama en la pregunta. Básicamente, representa todo lo que ejecuta código de forma asincrónica y produce un resultado que necesita ser recibido. Entonces podría ser como una solicitud AJAX. Se pasa la callbackfunción a la myAsynchronousCallfunción, que hace sus tareas asíncronas e invoca la devolución de llamada cuando se completa. Aquí hay una demostración.
60

Las funciones asíncronas , una característica de ES2017 , hacen que el código asíncrono se vea sincronizado mediante el uso de promesas (una forma particular de código asíncrono) y la awaitpalabra clave. Observe también en los ejemplos de código debajo de la palabra clave asyncdelante de la functionpalabra clave que significa una función asíncrona / espera. La awaitpalabra clave no funcionará sin estar en una función preestablecida con la asyncpalabra clave. Dado que actualmente no hay ninguna excepción a esto, eso significa que no funcionará ninguna espera de nivel superior (el nivel superior espera, lo que significa una espera fuera de cualquier función). Aunque hay una propuesta para el nivel superiorawait .

ES2017 fue ratificado (es decir, finalizado) como el estándar para JavaScript el 27 de junio de 2017. Async aguardando ya puede funcionar en su navegador, pero si no puede seguir utilizando la funcionalidad utilizando un transpiler javascript como babel o traceur . Chrome 55 tiene soporte completo de funciones asíncronas. Entonces, si tiene un navegador más nuevo, puede probar el código a continuación.

Consulte la tabla de compatibilidad de kangax es2017 para ver la compatibilidad del navegador.

Aquí hay un ejemplo de función de espera asíncrona llamada doAsyncque toma tres pausas de un segundo e imprime la diferencia horaria después de cada pausa desde la hora de inicio:

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

async function doAsync () {
  var start = Date.now(), time;
  console.log(0);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
  time = await doSomethingAsync();
  console.log(time - start);
}

doAsync();

Cuando la palabra clave de espera se coloca antes de un valor de promesa (en este caso, el valor de promesa es el valor devuelto por la función doSomethingAsync), la palabra clave de espera detendrá la ejecución de la llamada de función, pero no detendrá ninguna otra función y continuará ejecutando otro código hasta que se resuelva la promesa. Después de que la promesa se resuelva, desenvolverá el valor de la promesa y puede pensar en la expresión de esperar y prometer como ahora reemplazada por ese valor desenvuelto.

Entonces, dado que esperar solo pausa, espera y luego desenvuelve un valor antes de ejecutar el resto de la línea, puede usarlo para bucles y llamadas a funciones internas como en el ejemplo a continuación, que recopila las diferencias de tiempo esperadas en un conjunto e imprime el conjunto.

function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this calls each promise returning function one after the other
async function doAsync () {
  var response = [];
  var start = Date.now();
  // each index is a promise returning function
  var promiseFuncs= [doSomethingAsync, doSomethingAsync, doSomethingAsync];
  for(var i = 0; i < promiseFuncs.length; ++i) {
    var promiseFunc = promiseFuncs[i];
    response.push(await promiseFunc() - start);
    console.log(response);
  }
  // do something with response which is an array of values that were from resolved promises.
  return response
}

doAsync().then(function (response) {
  console.log(response)
})

La función asíncrona en sí misma devuelve una promesa, por lo que puede usarla como una promesa con el encadenamiento como lo hago arriba o dentro de otra función de espera asíncrona.

La función anterior esperaría cada respuesta antes de enviar otra solicitud. Si desea enviar las solicitudes simultáneamente, puede usar Promise.all .

// no change
function timeoutPromise (time) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      resolve(Date.now());
    }, time)
  })
}

// no change
function doSomethingAsync () {
  return timeoutPromise(1000);
}

// this function calls the async promise returning functions all at around the same time
async function doAsync () {
  var start = Date.now();
  // we are now using promise all to await all promises to settle
  var responses = await Promise.all([doSomethingAsync(), doSomethingAsync(), doSomethingAsync()]);
  return responses.map(x=>x-start);
}

// no change
doAsync().then(function (response) {
  console.log(response)
})

Si la promesa posiblemente se rechaza, puede envolverla en un try catch u omitir el try catch y dejar que el error se propague a la llamada catch de las funciones asíncronas / en espera. Debe tener cuidado de no dejar errores de promesa no controlados, especialmente en Node.js. A continuación se muestran algunos ejemplos que muestran cómo funcionan los errores.

function timeoutReject (time) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      reject(new Error("OOPS well you got an error at TIMESTAMP: " + Date.now()));
    }, time)
  })
}

function doErrorAsync () {
  return timeoutReject(1000);
}

var log = (...args)=>console.log(...args);
var logErr = (...args)=>console.error(...args);

async function unpropogatedError () {
  // promise is not awaited or returned so it does not propogate the error
  doErrorAsync();
  return "finished unpropogatedError successfully";
}

unpropogatedError().then(log).catch(logErr)

async function handledError () {
  var start = Date.now();
  try {
    console.log((await doErrorAsync()) - start);
    console.log("past error");
  } catch (e) {
    console.log("in catch we handled the error");
  }
  
  return "finished handledError successfully";
}

handledError().then(log).catch(logErr)

// example of how error propogates to chained catch method
async function propogatedError () {
  var start = Date.now();
  var time = await doErrorAsync() - start;
  console.log(time - start);
  return "finished propogatedError successfully";
}

// this is what prints propogatedError's error.
propogatedError().then(log).catch(logErr)

Si va aquí , puede ver las propuestas terminadas para las próximas versiones de ECMAScript.

Una alternativa a esto que se puede usar solo con ES2015 (ES6) es usar una función especial que envuelva una función de generador. Las funciones generadoras tienen una palabra clave de rendimiento que puede usarse para replicar la palabra clave de espera con una función circundante. La palabra clave de rendimiento y la función de generador tienen un propósito mucho más general y pueden hacer muchas más cosas que lo que hace la función de espera asíncrona. Si quieres un envoltorio función de generador que se puede utilizar para la réplica asíncrona esperan que volvería a la salida co.js . Por cierto, la función de co, al igual que las funciones de espera asíncrona, devuelve una promesa. Honestamente, en este punto, la compatibilidad del navegador es casi la misma tanto para las funciones de generador como para las funciones asíncronas, por lo que si solo desea la funcionalidad de espera asíncrona, debe usar las funciones asíncronas sin co.js.

El soporte del navegador es bastante bueno ahora para las funciones asíncronas (a partir de 2017) en todos los principales navegadores actuales (Chrome, Safari y Edge) excepto IE.

Juan
fuente
2
Me gusta esta respuesta
ycomp
1
qué tan lejos hemos llegado :)
Derek
3
Esta es una gran respuesta, pero para el problema de los pósters originales, creo que todo lo que hace es mover el problema a un nivel superior. Digamos que convierte doSomething en una función asíncrona con una espera dentro. Esa función ahora devuelve una promesa y es asíncrona, por lo que tendrá que lidiar con el mismo problema una vez más en lo que se llame esa función.
dpwrussell
1
@dpwrussell esto es cierto, hay un arrastramiento de funciones asíncronas y promesas en la base del código. La mejor manera de resolver las promesas de arrastrarse a todo es simplemente escribir devoluciones de llamada sincrónicas; no hay forma de devolver un valor asíncrono sincrónicamente a menos que haga algo extremadamente extraño y controvertido como esto twitter.com/sebmarkbage/status/941214259505119232 que no hago recomendar. Agregaré una edición al final de la pregunta para responder más completamente la pregunta tal como se hizo y no solo responder el título.
John
Es una gran respuesta +1 y todo, pero escrito como está, no veo cómo esto sea menos complicado que usar devoluciones de llamada.
Altimus Prime
47

Echa un vistazo a JQuery Promises:

http://api.jquery.com/promise/

http://api.jquery.com/jQuery.when/

http://api.jquery.com/deferred.promise/

Refactorizar el código:

    var dfd = new jQuery.Deferred ();


    función callBack (datos) {
       dfd.notify (datos);
    }

    // hacer la llamada asincrónica.
    myAsynchronousCall (param1, callBack);

    función doSomething (datos) {
     // hacer cosas con datos ...
    }

    $ .when (dfd) .then (doSomething);


Matt Taylor
fuente
3
+1 para esta respuesta, esto es correcto. Sin embargo, me gustaría actualizar la línea con dfd.notify(data)adfd.resolve(data)
Jason
77
¿Es este el caso del código que da la ilusión de ser síncrono, sin que en realidad NO sea asíncrono?
saurshaz
2
las promesas son IMO simplemente devoluciones de llamada bien organizadas :) si necesita una llamada asincrónica, digamos alguna inicialización de objeto, las promesas hacen una pequeña diferencia.
webduvet
10
Las promesas no son sincronizadas.
Furgonetas S
6

Hay una buena solución en http://taskjs.org/

Utiliza generadores que son nuevos en javascript. Por lo tanto, actualmente no está implementado por la mayoría de los navegadores. Lo probé en Firefox, y para mí es una buena manera de ajustar la función asincrónica.

Aquí hay un código de ejemplo del proyecto GitHub

var { Deferred } = task;

spawn(function() {
    out.innerHTML = "reading...\n";
    try {
        var d = yield read("read.html");
        alert(d.responseText.length);
    } catch (e) {
        e.stack.split(/\n/).forEach(function(line) { console.log(line) });
        console.log("");
        out.innerHTML = "error: " + e;
    }

});

function read(url, method) {
    method = method || "GET";
    var xhr = new XMLHttpRequest();
    var deferred = new Deferred();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4) {
            if (xhr.status >= 400) {
                var e = new Error(xhr.statusText);
                e.status = xhr.status;
                deferred.reject(e);
            } else {
                deferred.resolve({
                    responseText: xhr.responseText
                });
            }
        }
    };
    xhr.open(method, url, true);
    xhr.send();
    return deferred.promise;
}
George Vinokhodov
fuente
3

Usted puede forzar JavaScript asíncrono en NodeJS ser sincronizado con sincronización-RPC .

Sin embargo, definitivamente congelará tu interfaz de usuario, por lo que todavía soy una detractora cuando se trata de si es posible tomar el atajo que debes tomar. No es posible suspender el One And Only Thread en JavaScript, incluso si NodeJS le permite bloquearlo a veces. No se podrán procesar devoluciones de llamada, eventos, nada asíncrono hasta que se resuelva su promesa. Entonces, a menos que usted, el lector, tenga una situación inevitable como el OP (o, en mi caso, esté escribiendo un script de shell glorificado sin devoluciones de llamada, eventos, etc.), ¡NO HAGA ESTO!

Pero así es como puedes hacer esto:

./calling-file.js

var createClient = require('sync-rpc');
var mySynchronousCall = createClient(require.resolve('./my-asynchronous-call'), 'init data');

var param1 = 'test data'
var data = mySynchronousCall(param1);
console.log(data); // prints: received "test data" after "init data"

./my-asynchronous-call.js

function init(initData) {
  return function(param1) {
    // Return a promise here and the resulting rpc client will be synchronous
    return Promise.resolve('received "' + param1 + '" after "' + initData + '"');
  };
}
module.exports = init;

LIMITACIONES

Ambos son una consecuencia de cómo sync-rpcse implementa, que es abusando de require('child_process').spawnSync:

  1. Esto no funcionará en el navegador.
  2. Los argumentos de su función deben ser serializables. Sus argumentos entrarán y saldrán JSON.stringify, por lo que se perderán funciones y propiedades no enumerables como cadenas de prototipos.
meustrus
fuente
1

También puede convertirlo en devoluciones de llamada.

function thirdPartyFoo(callback) {    
  callback("Hello World");    
}

function foo() {    
  var fooVariable;

  thirdPartyFoo(function(data) {
    fooVariable = data;
  });

  return fooVariable;
}

var temp = foo();  
console.log(temp);
Nikhil
fuente
0

Lo que quieres es realmente posible ahora. Si puede ejecutar el código asincrónico en un trabajador de servicio y el código síncrono en un trabajador web, entonces puede hacer que el trabajador web envíe un XHR sincrónico al trabajador de servicio, y mientras el trabajador de servicio hace las cosas asíncronas, el trabajador web El hilo esperará. Este no es un gran enfoque, pero podría funcionar.

Quizás veas este nombre.
fuente
-4

La idea que espera lograr puede hacerse posible si modifica un poco el requisito

El siguiente código es posible si su tiempo de ejecución es compatible con la especificación ES6.

Más acerca de las funciones asíncronas

async function myAsynchronousCall(param1) {
    // logic for myAsynchronous call
    return d;
}

function doSomething() {

  var data = await myAsynchronousCall(param1); //'blocks' here until the async call is finished
  return data;
}
eragon512
fuente
44
Firefox da el error: SyntaxError: await is only valid in async functions and async generators. Sin mencionar que param1 no está definido (y ni siquiera se usa).
Harvey