Pregunta ¿Hay alguna forma de cortocircuitar el flujo asincrónico / esperar?


async function update() {
   var urls = await getCdnUrls();
   var metadata = await fetchMetaData(urls);
   var content = await fetchContent(metadata);
   await render(content);
   return;
}
//All the four functions return a promise. (getCdnUrls, fetchMetaData, fetchContent, render)

¿Qué pasa si queremos abortar la secuencia desde afuera, en cualquier momento?

Digamos, cuando fetchMetaData se está ejecutando, nos damos cuenta de que el componente ya no es necesario para ser renderizado y queremos cancelar las operaciones restantes (fetchContent y render). ¿Hay alguna manera de cancelar / cancelar desde el exterior por el consumidor?

Podríamos verificar después de cada espera de una condición, pero eso parece una manera poco elegante de hacer esto. y aún esperará a que finalice la operación actual.


14
2018-06-03 22:06


origen


Respuestas:


Acabo de dar una charla sobre esto: este es un tema encantador, pero lamentablemente no le van a gustar las soluciones que voy a proponer, ya que son soluciones de puerta de enlace.

Qué hace la especificación para ti

Obtener la cancelación "correcta" es realmente muy difícil. La gente ha estado trabajando en eso por un tiempo y se decidió no bloquear las funciones asincrónicas en él.

Hay dos propuestas que intentan resolver esto en el núcleo de ECMAScript:

  • Tokens de cancelación - que agrega tokens de cancelación que tienen como objetivo resolver este problema.
  • Promesa cancelable - que agrega catch cancel (e) { sintaxis y throw.cancel sintaxis que pretende abordar este problema.

Ambas propuestas cambiaron sustancialmente durante la última semana así que no esperaría llegar el próximo año más o menos. Las propuestas son de alguna manera complementarias y no están en desacuerdo.

Qué puedes hacer para resolver esto desde tu lado

Los tokens de cancelación son fáciles de implementar. Tristemente el tipo de cancelación que tendrías De Verdad querer (aka "aka"tercer estado la cancelación donde la cancelación no es una excepción) es imposible con las funciones asíncronas en este momento, ya que no se controla la forma en que se ejecutan. Puedes hacer dos cosas:

  • Use corutinas en su lugar - azulejo se envía con cancelación de sonido utilizando generadores y promesas que puede usar.
  • Implementar tokens con semántica abortiva: esto es bastante fácil, así que hagámoslo aquí

CancelaciónTokens

Bueno, una señal de token cancela:

class Token {
   constructor(fn) {
      this.isCancellationRequested = false; 
      this.onCancelled = []; // actions to execute when cancelled
      this.onCancelled.push(() => this.isCancellationRequested = true);
      // expose a promise to the outside
      this.promise = new Promise(resolve => this.onCancelled.push(resolve));
      // let the user add handlers
      fn(f => this.onCancelled.push(f));
   }
   cancel() { this.onCancelled.forEach(x => x); }
}

Esto te permitiría hacer algo como:

async function update(token) {
   if(token.isCancellationRequested) return;
   var urls = await getCdnUrls();
   if(token.isCancellationRequested) return;
   var metadata = await fetchMetaData(urls);
   if(token.isCancellationRequested) return;
   var content = await fetchContent(metadata);
   if(token.isCancellationRequested) return;
   await render(content);
   return;
}

var token = new Token(); // don't ned any special handling here
update(token);
// ...
if(updateNotNeeded) token.cancel(); // will abort asynchronous actions

Lo cual es una manera realmente fea que funcionaría, de manera óptima querrías que las funciones asíncronas estuvieran al tanto de esto, pero no lo son (todavía)

De manera óptima, todas sus funciones provisionales estarían al tanto y lo harían throw en la cancelación (de nuevo, solo porque no podemos tener un tercer estado) que se vería así:

async function update(token) {
   var urls = await getCdnUrls(token);
   var metadata = await fetchMetaData(urls, token);
   var content = await fetchContent(metadata, token);
   await render(content, token);
   return;
}

Dado que cada una de nuestras funciones son conscientes de la cancelación, pueden realizar una cancelación lógica real: getCdnUrls puede abortar la solicitud y lanzar, fetchMetaData puede abortar la solicitud subyacente y arrojar, etc.

Aquí es cómo uno podría escribir getCdnUrl (note el singular) usando el XMLHttpRequest API en navegadores:

function getCdnUrl(url, token) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    var p = new Promise((resolve, reject) => {
      xhr.onload = () => resolve(xhr);
      xhr.onerror = e => reject(new Error(e));
      token.promise.then(x => { 
        try { xhr.abort(); } catch(e) {}; // ignore abort errors
        reject(new Error("cancelled"));
      });
   });
   xhr.send();
   return p;
}

Esto es lo más cerca que podemos llegar de las funciones asincrónicas sin corutinas. No es muy bonito, pero ciertamente es útil.

Tenga en cuenta que desea evitar que las cancelaciones se traten como excepciones. Esto significa que si tus funciones throwen la cancelación, debe filtrar esos errores en los controladores de errores globales process.on("unhandledRejection", e => ... y tal.


12
2018-06-05 12:46



Puedes obtener lo que quieras usando Typescript + Bluebird + anulable-cancelable.

Ahora que todas las pruebas apuntan a tokens de cancelación no llegar a ECMAScript, Creo que la mejor solución para las cancelaciones es la implementación de Bluebird mencionada por @BenjaminGruenbaumSin embargo, encuentro que el uso de correlpancias y generadores es un poco torpe e incómodo para los ojos.

Dado que estoy usando Typescript, que ahora admite la sintaxis async / await para los objetivos es5 y es3, he creado un módulo simple que reemplaza el predeterminado __awaiter ayudante con uno que admite cancelaciones de bluebird: https://www.npmjs.com/package/cancelable-awaiter


3
2017-12-24 16:22



Desafortunadamente, no, no se puede controlar el flujo de ejecución del comportamiento async / await predeterminado, no significa que el problema en sí sea imposible, significa que debe cambiar un poco su enfoque.

En primer lugar, su propuesta sobre el ajuste de cada línea asíncrona en un cheque es una solución funcional, y si tiene solo dos lugares con dicha funcionalidad, no tiene nada de malo.

Si desea utilizar este patrón con bastante frecuencia, la mejor solución, probablemente, es para cambiar a generadores: aunque no están tan extendidos, le permiten definir el comportamiento de cada paso, y agregar cancelar es el más fácil. Los generadores son bastante poderoso, pero, como he mencionado, requieren una función de corredor y no tan sencillo como async / await.

Otro enfoque es crear patrón de tokens cancelable - creas un objeto, que se llenará con una función que quiere implementar esta funcionalidad:

async function updateUser(token) {
  let cancelled = false;

  // we don't reject, since we don't have access to
  // the returned promise
  // so we just don't call other functions, and reject
  // in the end
  token.cancel = () => {
    cancelled = true;
  };

  const data = await wrapWithCancel(fetchData)();
  const userData = await wrapWithCancel(updateUserData)(data);
  const userAddress = await wrapWithCancel(updateUserAddress)(userData);
  const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);

  // because we've wrapped all functions, in case of cancellations
  // we'll just fall through to this point, without calling any of
  // actual functions. We also can't reject by ourselves, since
  // we don't have control over returned promise
  if (cancelled) {
    throw { reason: 'cancelled' };
  }

  return marketingData;

  function wrapWithCancel(fn) {
    return data => {
      if (!cancelled) {
        return fn(data);
      }
    }
  }
}

const token = {};
const promise = updateUser(token);
// wait some time...
token.cancel(); // user will be updated any way

He escrito artículos, tanto sobre cancelación como sobre generadores:

Para resumir, debe realizar un trabajo adicional para admitir canncellation, y si desea tenerlo como ciudadano de primera clase en su aplicación, debe usar generadores.


0
2018-05-12 07:32



Al igual que en el código normal, debe lanzar una excepción desde la primera función (o cada una de las siguientes funciones) y probar bloqueando todo el conjunto de llamadas. No es necesario tener más if-elses. Esa es una de las cosas buenas de async / await, que te permite mantener el manejo de errores como usamos desde el código normal.

Wrt cancelar las otras operaciones no es necesario. En realidad, no comenzarán hasta que el intérprete encuentre sus expresiones. Por lo tanto, la segunda llamada asincrónica solo comenzará después de que termine la primera, sin errores. Otras tareas pueden tener la oportunidad de ejecutarse mientras tanto, pero para todos los efectos, esta sección de código es serial y se ejecutará en el orden deseado.


-1
2018-06-04 13:02