有没有办法短路async/await流?

Is there a way to short circuit async/await flow?

下面 update return promises 中调用的所有四个函数。

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

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

例如,在执行fetchMetaData时,我们意识到我们不再需要渲染组件,我们想取消剩余的操作(fetchContentrender)。有没有办法从 update 函数外部 abort/cancel 这些操作?

我们可以在每个 await 之后检查一个条件,但这似乎是一个不优雅的解决方案,即使那样我们也必须等待当前操作完成。

就像在常规代码中一样,您应该从第一个函数(或接下来的每个函数)中抛出异常,并在整个调用集周围有一个 try 块。不需要额外的 if-else。这是 async/await 的优点之一,您可以按照我们习惯的常规代码方式处理错误。

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

现在执行此操作的标准方法是通过 AbortSignals

async function update({ signal } = {}) {
   // pass these to methods to cancel them internally in turn
   // this is implemented throughout Node.js and most of the web platform
   try {
     var urls = await getCdnUrls({ signal });
     var metadata = await fetchMetaData(urls);
     var content = await fetchContent(metadata);
     await render(content);
   } catch (e) {
      if(e.name !== 'AbortError') throw e;
   }
   return;
}
// usage
const ac = new AbortController();
update({ signal: ac.signal });
ac.abort(); // cancel the update

下面是 2016 年的旧内容,小心龙

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

规范为您做什么

“恰到好处”地取消实际上非常困难。人们已经为此工作了一段时间,并决定不阻止其上的异步功能。

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

这两项提案在上周都发生了重大变化,所以我不指望它们能在明年左右到达。这些提议有些互补,并不矛盾。

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

取消令牌很容易实现。遗憾的是,您 真正 想要的那种取消(又名“third state 取消,其中取消也不例外)目前对于异步函数是不可能的,因为您无法控制他们怎么样 运行。你可以做两件事:

  • 改用协程 - bluebird 使用生成器和 promises 附带声音消除功能,您可以使用。
  • 用流产语义实现标记——这实际上很容易所以让我们在这里做

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 可以中止底层请求并抛出等等。

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

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 => ... 等上过滤这些错误。

你可以使用 Typescript + Bluebird + cancelable-awaiter.

得到你想要的

现在所有证据都指向取消标记 not making it to ECMAScript, I think the best solution for cancellations is the bluebird implementation mentioned by ,但是,我发现协同例程和生成器的使用有点笨拙,而且看起来不舒服。

因为我使用的是 Typescript,它现在支持 es5 和 es3 目标的 async/await 语法,所以我创建了一个简单的模块,用支持 bluebird 取消的模块替换了默认的 __awaiter 帮助程序: https://www.npmjs.com/package/cancelable-awaiter

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

首先,您关于在支票中包装每条异步行的建议是一个可行的解决方案,如果您只有几个地方具有这种功能,那么它没有任何问题。

如果您想经常使用此模式,最好的解决方案可能是 to switch to generators: while not so widespread, they allow you to define each step's behaviour, and adding cancel is the easiest. Generators are 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

我写过关于取消和生成器的文章:

总而言之——你必须做一些额外的工作来支持取消,如果你想把它作为应用程序中的第一个 class 公民,你必须使用生成器。

这是一个简单的例子,有一个承诺:

let resp = await new Promise(function(resolve, reject) {
    // simulating time consuming process
    setTimeout(() => resolve('Promise RESOLVED !'), 3000);
    // hit a button to cancel the promise
    $('#btn').click(() => resolve('Promise CANCELED !'));
});

请参阅此 codepen 进行演示

使用 Typescript 在 Node 中编写的可以从外部中止的调用示例:

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

可能会帮助您将函数重写为:

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

但是我还没有实现“class-preserving”then 函数,所以目前你必须用 comPromise.race.[=14 包装你想要取消的每个部分=]

遗憾的是,目前还没有 cancellable promises 的支持。有一些自定义实现,例如

Extends/wraps 可取消和可解决的承诺


function promisify(promise) {
  let _resolve, _reject

  let wrap = new Promise(async (resolve, reject) => {
    _resolve = resolve
    _reject = reject
    let result = await promise
    resolve(result)
  })

  wrap.resolve = _resolve
  wrap.reject = _reject
    
  return wrap
}

用法:取消承诺并在它之后立即停止进一步执行

async function test() {
  // Create promise that should be resolved in 3 seconds
  let promise = new Promise(resolve => setTimeout(() => resolve('our resolved value'), 3000))
  
  // extend our promise to be cancellable
  let cancellablePromise = promisify(promise)
  
  // Cancel promise in 2 seconds.
  // if you comment this line out, then promise will be resolved.
  setTimeout(() => cancellablePromise.reject('error code'), 2000)

  // wait promise to be resolved
  let result = await cancellablePromise
  
  // this line will never be executed!
  console.log(result)
}

在这种方法中,promise 本身会执行到最后,但是等待 promise 结果的调用者代码可以是 'cancelled'。

使用 CPromise (c-promise2 package) 这可以通过以下方式轻松完成 (Demo):

import CPromise from "c-promise2";

async function getCdnUrls() {
  console.log(`task1:start`);
  await CPromise.delay(1000);
  console.log(`task1:end`);
}

async function fetchMetaData() {
  console.log(`task2:start`);
  await CPromise.delay(1000);
  console.log(`task2:end`);
}

function* fetchContent() {
  // using generators is the recommended way to write asynchronous code with CPromise
  console.log(`task3:start`);
  yield CPromise.delay(1000);
  console.log(`task3:end`);
}

function* render() {
  console.log(`task4:start`);
  yield CPromise.delay(1000);
  console.log(`task4:end`);
}

const update = CPromise.promisify(function* () {
  var urls = yield getCdnUrls();
  var metadata = yield fetchMetaData(urls);
  var content = yield* fetchContent(metadata);
  yield* render(content);
  return 123;
});

const promise = update().then(
  (v) => console.log(`Done: ${v}`),
  (e) => console.warn(`Fail: ${e}`)
);

setTimeout(() => promise.cancel(), 2500);

控制台输出:

task1:start 
task1:end 
task2:start 
task2:end 
task3:start 
Fail: CanceledError: canceled 

我创建了一个名为 @kaisukez/cancellation-token

的库

想法是将 CancellationToken 传递给每个异步函数,然后将每个 promise 包装在 AsyncCheckpoint 中。这样当token被取消的时候,你的async函数就会在下一个checkpoint被取消。

这个想法来自tc39/proposal-cancelable-promises conradreuter/cancellationtoken.


如何使用我的图书馆

  1. 重构您的代码
// from this
async function yourFunction(param1, param2) {
    const result1 = await someAsyncFunction1(param1)
    const result2 = await someAsyncFunction2(param2)
    return [result1, result2]
}

// to this
import { AsyncCheckpoint } from '@kaisukez/cancellation-token'
async function yourFunction(token, param1, param2) {
    const result1 = await AsyncCheckpoint.after(token, () => someAsyncFunction1(param1))
    const result2 = await AsyncCheckpoint.after(token, () => someAsyncFunction2(param2))
    return [result1, result2]
}
  1. 创建一个令牌,然后使用该令牌调用您的函数
import { CancellationToken, CancellationError } from '@kaisukez/cancellation-token'

const [token, cancel] = CancellationToken.source()

// spawn background task (run async function without using `await`)
CancellationError.ignoreAsync(() => yourAsyncFunction(token, param1, param2))

// ... do something ...

// then cancel the background task
await cancel()

所以这是OP问题的解决方案。

import { CancellationToken, CancellationError, AsyncCheckpoint } from '@kaisukez/cancellation-token'

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

const [token, cancel] = CancellationToken.source();

// spawn background task (run async function without using `await`)
CancellationError.ignoreAsync(() => update(token))

// ... do something ...

// then cancel the background task
await cancel()