有没有一种方法来短路异步/等待流?

所有这四个函数在 update返回承诺中调用如下。

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

如果我们想在任何给定时间从外部终止序列会怎样?

例如,当执行 fetchMetaData时,我们意识到我们不再需要呈现组件,我们想取消剩余的操作(fetchContentrender)。有没有办法从 update函数之外中止/取消这些操作?

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

42680 次浏览

就像在普通代码中一样,您应该从第一个函数(或每个下一个函数)抛出一个异常,并在整个调用集周围设置 try 块。不需要额外的如果-别的。这是关于异步/等待的好处之一,您可以按照我们习惯的方式处理常规代码中的错误。

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

现在执行此操作的标准方法是通过中止信号

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 核心中解决这个问题:

  • 取消令牌 -添加旨在解决此问题的取消令牌。
  • 可取消承诺 -增加了 catch cancel (e) { 语法和 throw.cancel语法,旨在解决这个问题。

这两个提案大幅度改变了 在过去的一周里,所以我不指望它们中的任何一个在明年左右到来。这些建议在一定程度上是赞扬性的,并不矛盾。

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

取消令牌很容易实现。遗憾的是,真的想要的那种取消(也就是“ 第三州取消,取消不是例外情况)目前对异步函数来说是不可能的,因为您无法控制它们的运行方式。你可以做两件事:

  • 使用协同程序-青鸟船舶与声音消除使用生成器和承诺,你可以使用。
  • 使用流产语义实现标记-这实际上很容易,所以让我们在这里做

取消令牌

一个象征性的信号表示取消:

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

你可以使用类型脚本 + 蓝鸟 + 可取消服务员得到你想要的。

现在所有的证据都指向取消令牌 没有进入 ECMAScript,我认为取消的最佳解决方案是 @ Benjamin Gruenbaum提到的蓝鸟实现,然而,我发现协同例程和生成器的使用有点笨拙和令人不安的眼睛。

因为我使用的是 Type 脚本,它现在支持 es5和 es3目标的异步/等待语法,所以我创建了一个简单的模块,用一个支持蓝鸟取消的模块来替换默认的 __awaiter助手: https://www.npmjs.com/package/cancelable-awaiter

不幸的是,不能,您不能控制默认异步/等待行为的执行流程-这并不意味着问题本身是不可能的,它意味着您需要稍微改变一下您的方法。

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

如果您想经常使用这种模式,最好的解决方案可能是 切换到发电机: 虽然不是很普遍,但它们允许您定义每个步骤的行为,并且添加取消是最容易的。生成器是 非常强大,但是,正如我所提到的,它们需要一个运行器函数,并不像异步/等待那样简单。

另一种方法是创建 可取消令牌模式-你创建一个对象,它将被填充一个希望实现这个功能的函数:

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

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

总结一下——为了支持取消,您必须做一些额外的工作,如果您想让它成为应用程序中的一等公民,那么您必须使用生成器。

下面是一个 很简单的例子,它承诺:

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演示

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

不幸的是,到目前为止还没有对 cancellable承诺的支持。

扩展/包装可取消和可解决的承诺


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

在这种方法中,承诺本身执行到最后,但是等待承诺结果的调用方代码可以被“取消”。

这可以通过下面的方式轻松地使用 C 许诺(承诺二号包裹)来实现 (演示) :

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/取消令牌的库

其思想是向每个异步函数传递一个 CancellationToken,然后在 AsyncCheckpoint中包装每个承诺。这样,当令牌被取消时,您的异步函数将在下一个检查点中被取消。

这个想法来自于《建议-可取消的承诺》 和 取消/取消令牌


如何使用我的图书馆

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