有没有办法短路异步/等待流量?

时间:2016-06-03 22:06:48

标签: javascript promise async-await cancellation ecmascript-next

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)

如果我们想在任何时候从外部中止序列怎么办?

比如说,当执行fetchMetaData时,我们意识到不再需要渲染组件,我们想要取消剩余的操作(fetchContent和render)。 消费者有没有办法从外面中止/取消?

我们可以在每次等待一个条件后检查,但这似乎是一种不太优雅的方式。它仍然会等待当前的操作完成。

7 个答案:

答案 0 :(得分:20)

我刚刚谈了这个 - 这是一个可爱的主题,但遗憾的是你并不会真正喜欢我将提出的解决方案,因为它们是网关解决方案。

规范为您做什么

取消“恰到好处”实际上非常困难。人们一直在研究这个问题,并决定不阻止异步功能。

有两个提案试图在ECMAScript核心中解决这个问题:

这两个提案在上周中发生了重大变化所以我不会指望要么在明年左右到达。这些建议有点互补,并不存在争议。

你可以做些什么来解决这个问题

取消令牌很容易实现。遗憾的是,由于你不能控制它们的方式,你现在想要取消的那种取消(也就是取消不是例外的“third state取消”)是不可能的。你可以做两件事:

  • 使用协程代替 - bluebird使用您可以使用的生成器和承诺发出声音取消。
  • 使用流产语义实现令牌 - 这实际上非常简单,所以让我们在这里做吧

CancellationTokens

好吧,令牌信号取消:

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); }
}

这可以让你做类似的事情:

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

这是一种非常丑陋的方式可行,最佳的是你希望异步函数能够意识到这一点,但它们不是()。

最理想的是,你所有的临时函数都会注意到,throw取消(同样,只是因为我们不能有第三种状态)看起来像:

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;
}

由于我们的每个函数都可以识别取消,因此它们可以执行实际的逻辑取消 - getCdnUrls可以中止请求并抛出,fetchMetaData可以中止基础请求并抛出等等。

以下是在浏览器中使用getCdnUrl API撰写XMLHttpRequest(注意单数)的方法:

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;
}

这与没有协同程序的异步函数一样接近。它不是很漂亮但它确实可用。

请注意,您希望避免将取消视为异常。这意味着如果您的功能throw取消,您需要在全局错误处理程序process.on("unhandledRejection", e => ...上过滤这些错误等。

答案 1 :(得分:3)

你可以使用Typescript + Bluebird + cancelable-awaiter得到你想要的东西。

既然所有证据都指向取消令牌not making it to ECMAScript,我认为取消的最佳解决方案是@BenjaminGruenbaum提到的蓝鸟实现,但是,我发现了一些协同例程和生成器的使用眼睛笨拙而不安。

由于我正在使用现在支持es5和es3目标的async / await语法的Typescript,我创建了一个简单的模块,用一个支持bluebird取消的帮助器替换默认的__awaiter帮助器:{{3 }}

答案 2 :(得分:1)

不幸的是,不,你无法控制默认的async / await行为的执行流程 - 这并不意味着问题本身是不可能的,这意味着你需要稍微改变你的方法。

首先,你提出的关于在支票中包装每个异步行的建议是一个有效的解决方案,如果你只有几个地方有这样的功能,那就没有错。

如果你想经常使用这种模式,最好的解决方案可能是to switch to generators:虽然不是那么普遍,但它们允许你定义每个步骤的行为,添加取消是最简单的。生成器是pretty powerful,但正如我所提到的,它们需要一个运行器函数,而不是像async / await那样简单。

另一种方法是创建cancellable tokens pattern - 您创建一个对象,该对象将填充一个想要实现此功能的函数:

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

我写了关于取消和发电机的文章:

总结一下 - 你必须做一些额外的工作才能支持canncellation,如果你想在你的应用程序中将它作为头等公民,你必须使用生成器。

答案 3 :(得分:0)

这是一个简单示例,要求您创建新的承诺:

let resp = await new Promise(function(resolve, reject) {
    timeout(5000).then(() => resolve('Promise RESOLVED !'), reject);
    $('#btn').click(() => resolve('Promise CANCELED !'));
});

请参阅此codepen进行演示

答案 4 :(得分:0)

This answer I posted可以帮助您将函数重写为:

async function update() {
   var get_urls = comPromise.race([getCdnUrls()]);
   var get_metadata = get_urls.then(urls=>fetchMetaData(urls));
   var get_content = get_metadata.then(metadata=>fetchContent(metadata);
   var render = get_content.then(content=>render(content));
   await render;
   return;
}

// this is the cancel command so that later steps will never proceed:
get_urls.abort();

但是我还没有实现“保留类”的then函数,因此当前您必须包装要用comPromise.race取消的每个部分。

答案 5 :(得分:-1)

用Node编写的示例,带有可从外部中止的呼叫的Typescript:

function cancelable(asyncFunc: Promise<void>): [Promise<void>, () => boolean] {
  class CancelEmitter extends EventEmitter { }

  const cancelEmitter = new CancelEmitter();
  const promise = new Promise<void>(async (resolve, reject) => {

    cancelEmitter.on('cancel', () => {
      resolve();
    });

    try {
      await asyncFunc;
      resolve();
    } catch (err) {
      reject(err);
    }

  });

  return [promise, () => cancelEmitter.emit('cancel')];
}

用法:

const asyncFunction = async () => {
  // doSomething
}

const [promise, cancel] = cancelable(asyncFunction());

setTimeout(() => {
  cancel();
}, 2000);

(async () => await promise)();

答案 6 :(得分:-2)

就像在常规代码中一样,你应该从第一个函数(或每个下一个函数)抛出一个异常,并在整个调用集周围尝试一下。不需要额外的if-elses。这是关于async / await的不错的一点,你可以按照常规代码习惯的方式处理错误处理。

Wrt取消其他操作没有必要。在解释器遇到表达式之前,它们实际上不会启动。所以第二个异步调用只会在第一个异步调用完成后才开始,没有错误。其他任务可能在此期间有机会执行,但是出于所有意图和目的,这部分代码是串行的,将按所需顺序执行。