一种可取消的Promise:CancelablePromise
前置知识
一、背景
传统的Promise是没有中断机制的,一旦执行无论如何都会执行到底。但是实际的应用场景当中,有可能希望创建一个可中断的Promise,尽早中断相关的异步操作,避免继续浪费资源和时间。
二、实现
可取消Promise是基于Cancellation机制配合原生Promise进行实现的,它其实是Cancellation机制的一个实际应用封装。实际上通过createCancelablePromise()调用后获取到的CancelablePromise拥有原生Promise常用的then、catch、finally三个方法,而内部的实现无非就是这三个方法原路转发给原生Promise而已。而多出来的cancel方法就是新增的取消操作,核心逻辑还是Cancellation机制的取消操作,即使用cancellationTokenSource.cancel方法发起取消请求。
interface CancelablePromise<T> extends Promise<T> {
cancel(): void;
}
function createCancelablePromise<T>(callback: (token: CancellationToken) => Promise<T>): CancelablePromise<T> {
const source = new CancellationTokenSource();
const thenable = callback(source.token);
let isCancelled = false;
const promise = new Promise<T>((resolve, reject) => {
// 如果这个新创建的promise被取消了,那么该promise的处理结果会被丢弃,不会返回任何结果,而是得到一个`CancellationError`
const subscription = source.token.onCancellationRequested(() => {
isCancelled = true;
subscription.dispose();
reject(new CancellationError());
});
// 否则将根据thenable的执行情况执行promise的`then`或者`catch`。
// 这里使用Promise.resolve(thenable)是为了无论 callback 返回的是 Promise 还是自定义 thenable,先包装成原生 Promise
Promise.resolve(thenable).then(value => {
subscription.dispose();
source.dispose();
if (!isCancelled) {
resolve(value);
} else if (isDisposable(value)) {
value.dispose();
}
}, err => {
subscription.dispose();
source.dispose();
reject(err);
});
});
return <CancelablePromise<T>>new class {
cancel() {
source.cancel();
source.dispose();
}
then<TResult1 = T, TResult2 = never>(resolve?: ((value: T) => TResult1 | Promise<TResult1>) | undefined | null, reject?: ((reason: unknown) => TResult2 | Promise<TResult2>) | undefined | null): Promise<TResult1 | TResult2> {
return promise.then(resolve, reject);
}
catch<TResult = never>(reject?: ((reason: unknown) => TResult | Promise<TResult>) | undefined | null): Promise<T | TResult> {
return this.then(undefined, reject);
}
finally(onfinally?: (() => void) | undefined | null): Promise<T> {
return promise.finally(onfinally);
}
};
}重点理解的关键是外部通过cancelablePromise.cancel到cancellationTokenSource.cancel到cancellationToken.onCancellationRequested再到reject(new CancellationError())这条交互线控制的是代码中创建的promise对象状态的转变,而不是代码中thenable对象状态的转变。
对于外部调用方来说拿到的就是CancelablePromise对象,如果cancel发起,那么外部不会获得任何返回结果,而是得到一个CancellationError。这是因为CancelablePromise对象实际上只是内部创建的promise对象的一个代理包装而已,它的状态及结果全都由内部创建的promise对象的执行行为来决定。
那你可能会有疑问:cancelablePromise.cancel只是控制的内部创建的promise对象的状态更改,那么thenable对象的状态更改呢?按照当前说法不是脱裤子放屁吗?其实不然,仔细看我们在callback(source.token)时传入了source.token,这个异步任务的执行框架接收了CancellationToken,它能够在内部通过token.onCancellationRequested监听取消请求或者token.isCancellationRequested判断是否已经取消,从而决定是否继续执行相关异步任务。
我们进一步通过下图理解对应关系,调用方拿到的CancelablePromise实际上是通过createCancelablePromise工厂内部创建的promise的一个代理包装而已。
CancelablePromise的状态由内部这个promise的状态来决定。内部创建出来的promise通过两个行为途径来决定自身的状态改变:
token.onCancellationRequested:它在取消请求到来时最终将该promise转换到rejected状态。const thenable = callback(source.token):它自身状态确立后进一步决定将该promise转换到resolved状态或者是rejected状态,具体的状态确立由它自身执行的结果来决定。
调用方可以使用cancelablePromise.cancel发起取消请求到内部创建的promise,内部promise在创建之初就通过构造函数接收的executor里头通过token.onCancellationRequested监听外部调用方发来的取消请求,从而进一步将内部的promise状态转为rejected,从而将外部使用的这个cancelablePromise转为rejected状态。
三、总结
通过Cancellation机制配合Promise,我们就能够封装出CancelablePromise,核心思路就是在Promise中接收取消请求并进行处理,而处理的核心逻辑其实就是使用reject确定Promise的状态为rejected,从而抛出CancellationError完成Promise的中断。
NOTE
思考一个问题:Promise.resolve(promise)返回的Promise对象的状态到底是由谁控制的?
const thenable = new Promise(() => {});
const promise = Promise.resolve(thenable);上面代码,当Promise.resolve(thenable)时,其中thenable是一个Promise对象,则promise的状态由thenable来控制,thenable从pending转变为resolved,则promise的状态就从pending转变成resolved状态,此时如果接着有promise.then(fn),则随后会执行fn。这个问题的本质是Promise.resolve(promise)返回的就是promise本身,状态是共享的。