¿Cómo esperar a que se resuelva una promesa de JavaScript antes de reanudar la función?

107

Estoy haciendo algunas pruebas unitarias. El marco de prueba carga una página en un iFrame y luego ejecuta afirmaciones en esa página. Antes de que comience cada prueba, creo un Promiseque establece el onloadevento del iFrame para llamar resolve(), establece el iFrame srcy devuelve la promesa.

Entonces, puedo simplemente llamar loadUrl(url).then(myFunc), y esperará a que se cargue la página antes de ejecutar lo que myFuncsea.

Utilizo este tipo de patrón en todas partes en mis pruebas (no solo para cargar URLs), principalmente para permitir que ocurran cambios en el DOM (por ejemplo, imitar al hacer clic en un botón y esperar a que los divs se oculten y muestren).

La desventaja de este diseño es que constantemente escribo funciones anónimas con algunas líneas de código en ellas. Además, aunque tengo una solución alternativa (QUnit assert.async()), la función de prueba que define las promesas se completa antes de que se ejecute la promesa.

Me pregunto si hay alguna forma de obtener un valor de a Promiseo esperar (bloquear / dormir) hasta que se resuelva, similar a .NET IAsyncResult.WaitHandle.WaitOne(). Sé que JavaScript es de un solo subproceso, pero espero que eso no signifique que una función no pueda ceder.

En esencia, ¿hay alguna manera de obtener lo siguiente para escupir los resultados en el orden correcto?

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
};

function getResultFrom(promise) {
  // todo
  return " end";
}

var promise = kickOff();
var result = getResultFrom(promise);
$("#output").append(result);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

dx_over_dt
fuente
si coloca las llamadas anexas en una función reutilizable, puede entonces () según sea necesario para SECAR. también puede hacer controladores de usos múltiples, dirigidos por esto para alimentar luego () llamadas, como .then(fnAppend.bind(myDiv)), que pueden reducir enormemente los anones.
Dandavis
¿Con qué estás probando? Si es un navegador moderno o puede usar una herramienta como BabelJS para transpilar su código, esto es ciertamente posible.
Benjamin Gruenbaum

Respuestas:

78

Me pregunto si hay alguna manera de obtener un valor de una Promesa o esperar (bloquear / dormir) hasta que se resuelva, similar a IAsyncResult.WaitHandle.WaitOne () de .NET. Sé que JavaScript es de un solo subproceso, pero espero que eso no signifique que una función no pueda ceder.

La generación actual de Javascript en los navegadores no tiene un wait()o sleep()que permita que se ejecuten otras cosas. Entonces, simplemente no puede hacer lo que está pidiendo. En cambio, tiene operaciones asincrónicas que harán lo suyo y luego lo llamarán cuando hayan terminado (como ha estado usando promesas).

Parte de esto se debe al único subproceso de Javascript. Si el hilo único está girando, entonces ningún otro Javascript se puede ejecutar hasta que ese hilo giratorio esté terminado. ES6 presenta yieldy generadores que permitirán algunos trucos cooperativos como ese, pero estamos muy lejos de poder usarlos en una amplia muestra de navegadores instalados (se pueden usar en algunos desarrollos del lado del servidor donde controlas el motor JS que se está utilizando).


La administración cuidadosa del código basado en promesas puede controlar el orden de ejecución de muchas operaciones asíncronas.

No estoy seguro de entender exactamente qué orden está tratando de lograr en su código, pero podría hacer algo como esto usando su kickOff()función existente y luego adjuntarle un .then()controlador después de llamarlo:

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");

    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
}

kickOff().then(function(result) {
    // use the result here
    $("#output").append(result);
});

Esto devolverá la salida en un orden garantizado, como este:

start
middle
end

Actualización en 2018 (tres años después de que se escribió esta respuesta):

Si transpila su código o ejecuta su código en un entorno que admita características de ES7 como asyncy await, ahora puede usarlo awaitpara hacer que su código "parezca" esperar el resultado de una promesa. Todavía se está desarrollando con promesas. Aún no bloquea todo Javascript, pero le permite escribir operaciones secuenciales en una sintaxis más amigable.

En lugar de la forma de hacer las cosas de ES6:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

Puedes hacerlo:

// returns a promise
async function wrapperFunc() {
    try {
        let r1 = await someFunc();
        let r2 = await someFunc2(r1);
        // now process r2
        return someValue;     // this will be the resolved value of the returned promise
    } catch(e) {
        console.log(e);
        throw e;      // let caller know the promise was rejected with this reason
    }
}

wrapperFunc().then(result => {
    // got final result
}).catch(err => {
    // got error
});
jfriend00
fuente
3
Bien, consumir then()es lo que he estado haciendo. Simplemente no me gusta tener que escribir function() { ... }todo el tiempo. Desordena el código.
dx_over_dt
3
@dfoverdx: la codificación asíncrona en Javascript siempre implica devoluciones de llamada, por lo que siempre debe definir una función (anónima o con nombre). No hay forma de evitarlo actualmente.
jfriend00
2
Aunque puede usar la notación de flechas ES6 para hacerlo más conciso. (foo) => { ... }en lugar defunction(foo) { ... }
Rob H
1
Añadiendo al comentario de @RobH con bastante frecuencia, también puede escribir foo => single.expression(here)para deshacerse de los paréntesis rizados y redondos.
inicio
3
@dfoverdx: tres años después de mi respuesta, lo actualicé con ES7 asyncy la awaitsintaxis como una opción que ahora está disponible en node.js y en los navegadores modernos o mediante transpilers.
jfriend00
15

Si se utiliza ES2016 puede utilizar asyncy awaity hacer algo como:

(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

Si usa ES2015, puede usar generadores . Si no le gusta la sintaxis, puede abstraerla usando una asyncfunción de utilidad como se explica aquí .

Si usa ES5 probablemente querrá una biblioteca como Bluebird para darle más control.

Finalmente, si su tiempo de ejecución es compatible con ES2015, el orden de ejecución puede conservarse con paralelismo usando Fetch Injection .

Josh Habdas
fuente
1
Su ejemplo de generador no funciona, le falta un corredor. No recomiende más generadores cuando solo pueda transpilar ES8 async/ await.
Bergi
3
Gracias por tus comentarios. Eliminé el ejemplo de Generator y lo vinculé a un tutorial de Generator más completo con una biblioteca OSS relacionada.
Josh Habdas
9
Esto simplemente envuelve la promesa. La función async no esperará nada cuando la ejecute, solo devolverá una promesa. async/ awaites solo otra sintaxis de Promise.then.
rustyx
Tiene toda la razón async, no esperará ... a menos que ceda el uso await. El ejemplo de ES2016 / ES7 todavía no se puede ejecutar, así que aquí hay un código que escribí hace tres años para demostrar el comportamiento.
Josh Habdas
7

Otra opción es usar Promise.all para esperar a que se resuelva una serie de promesas. El código a continuación muestra cómo esperar a que se resuelvan todas las promesas y luego lidiar con los resultados una vez que estén todos listos (ya que ese parecía ser el objetivo de la pregunta); Sin embargo, el inicio podría obviamente salir antes de que el medio se haya resuelto simplemente agregando ese código antes de que llame a resolverse.

function kickOff() {
  let start = new Promise((resolve, reject) => resolve("start"))
  let middle = new Promise((resolve, reject) => {
    setTimeout(() => resolve(" middle"), 1000)
  })
  let end = new Promise((resolve, reject) => resolve(" end"))

  Promise.all([start, middle, end]).then(results => {
    results.forEach(result => $("#output").append(result))
  })
}

kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

Stan Kurdziel
fuente
1

Puedes hacerlo manualmente. (Lo sé, esa no es una gran solución, pero ...) use el whilebucle hasta que resultno tenga un valor

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});
Guga Nemsitsveridze
fuente
esto consumiría toda la CPU mientras espera.
Lukasz Guminski
esta es una solución horrible
JSON