如何中断或取消承诺以清理在 JavaScript/TypeScript 中异步创建的多个对象

How to interrupt or cancel promises in order to clean up multiple objects created asynchronously in JavaScript/TypeScript

注意:我的代码恰好是用 TypeScript 编写的,但是这个问题适用于 TypeScript 和 JavaScript,只是有一些语法上的变化。

我有一个 class 可以订阅一些东西(例如高速公路消息)。当我处理完该对象后,我需要取消订阅所有内容。一般来说,我的代码是这样的:

class Example {
    private sub1: Subscription;
    private sub2: Subscription;
    private sub3: Subscription;

    constructor() {
        subscribeToThings();
    }

    async subscribeToThings() {
        this.sub1 = await subscribeToThingOne();
        this.sub2 = await subscribeToThingTwo();
        // *** NOTE 1 ***
        this.sub3 = await subscribeToThingThree();
    }

    cleanupOnDeath () {
        this.sub1.unsubscribe();
        this.sub2.unsubscribe();
        this.sub3.unsubscribe();
    }
}

当我不再需要这个对象时,我调用cleanupOnDeath

但是,有一个问题。如果订阅(异步的)需要很长时间,则可能会在设置所有 sub1sub2sub3 之前调用 "cleanupOnDeath()" 函数。例如,当调用 cleanupOnDeath 时,代码可能位于标记为 *** NOTE 1 *** 的行。

当然,我可以在调用 unsubscribe 之前检查每个是否未定义,但这并不能真正解决问题,因为那时第三个订阅得到处理 after cleanupOnDeath 完成,并永远存在。

有没有办法实现此代码,以便可以在 subscribeToThings() 中间调用 cleanupOnDeath() 而不会使代码变得异常复杂?

我能想出的最简单的解决方案是这样的(根据需要替换上面代码的部分):

    constructor() {
        this.subscribeAndUnsubscribe();
    }

    async subscribeAndUnsubscribe () {
        await this.subscribeToThings();

        await cleanupEvent.wait();

        this.sub1.unsubscribe();
        this.sub2.unsubscribe();
        this.sub3.unsubscribe();
    }

    cleanupOnDeath() {
        cleanupEvent.set();
    }

其中 cleanupEvent 是某种同步原语。但我不认为这是最佳的,因为它还要求在取消订阅之前完成所有未决订阅。理想情况下,我希望能够提前中止 subscribeToThings(),而无需在每行代码之后添加检查。例如。可以这样做:

    completed:boolean = false;

    async subscribeToThings() {
        try {
           this.abortIfDone();
           this.sub1 = await subscribeToThingOne();
           this.abortIfDone();
           this.sub2 = await subscribeToThingTwo();
           this.abortIfDone();
           this.sub3 = await subscribeToThingThree();
        } catch (abortedEarly) {
           // Something here
        }
    }

    abortIfDone() {
        if (this.completed) throw 'Something';
    }

    cleanupOnDeath() {
        this.completed = true;
        if (this.sub1) this.sub1.unsubscribe();
        if (this.sub1) this.sub2.unsubscribe();
        if (this.sub1) this.sub3.unsubscribe();
    }

但那又乱又复杂。

编辑 - 解决方案

根据@ChrisTavares 的回答,我最终使用了以下代码。这和他的回答之间的区别在于我不再等待任何事情或自己存储订阅。之前,我一直在等待他们获取底层订阅对象,其唯一目的是稍后取消订阅。由于此代码存储承诺并处理使用 then 取消订阅的承诺,因此没有必要这样做。

class Example {
    private sub1Promise: Promise<Subscription>;
    private sub2Promise: Promise<Subscription>;
    private sub3Promise: Promise<Subscription>;

    constructor() {
        this.subscribeToThings();
    }

    subscribeToThings() {
        // No longer async!
        this.sub1Promise = subscribeToThingOne();
        this.sub2Promise = subscribeToThingTwo();
        this.sub3Promise = subscribeToThingThree();
    }

    cleanupOnDeath () {
        this.sub1Promise.then(s => s.unsubscribe());
        this.sub2Promise.then(s => s.unsubscribe());
        this.sub3Promise.then(s => s.unsubscribe());
    }
}

没有取消承诺的标准方法 - 各个规范委员会都在进行讨论,但这是一个难题,原因有很多,在这里无济于事。

我怀疑这是否是一个合理的问题 - 订阅真的慢到足以担心这个问题吗?我对高速公路不熟悉。

但是,假设是这样,可以做的一件事是不要立即 await 东西,而是坚持实际的承诺。然后你可以在需要的时候添加一个 .then 处理程序来清理东西。像这样:

class Example {
    private sub1: Subscription;
    private sub2: Subscription;
    private sub3: Subscription;

    private sub1Promise: Promise<Subscription>;
    private sub2Promise: Promise<Subscription>;
    private sub3Promise: Promise<Subscription>;

    constructor() {
        subscribeToThings();
    }

    async subscribeToThings() {
        // *** NOTE - no await here ***
        this.sub1Promise = subscribeToThingOne();
        this.sub2Promise = subscribeToThingTwo();
        this.sub3Promise = subscribeToThingThree();

        this.sub1 = await this.sub1Promise;
        this.sub2 = await this.sub2Promise;
        this.sub3 = await this.sub3Promise;
    }

    cleanupOnDeath () {
        // Unsubscribe each promise. Don't need to check for null,
        // they were set in subscribeToThings
        this.sub1Promise.then((s) => s.unsubscribe());
        this.sub2Promise.then((s) => s.unsubscribe());
        this.sub3Promise.then((s) => s.unsubscribe());
    }
}

另一种解决方案是通过同步执行器 运行 subscribeToThings 方法的逻辑 nsynjs。它不是基于承诺的,因此无需取消承诺。但它目前使用回调跟踪 运行ning 缓慢的函数,并允许释放被它们占用的资源(例如发出 xhr.abort 或 clearTimeout)以防调用者函数被请求停止。这是如何完成的:

第 1 步。将带有回调的底层函数包装到 nsynjs 感知包装器中。请参阅此示例中的 waitajaxGetJson 函数:https://github.com/amaksr/nsynjs/blob/master/examples/browser-ajax-seq.js

第 2 步。将订阅逻辑放入单独的函数中:

function syn_SubscribeToThings() {
    function subscribeToThingOne() {
        ...
        var data = ajaxGetJson(nsynjsCtx,'subscribe/url1').data;
        ...
    };
    function subscribeToThingOneTwo {
        ...
        var data = ajaxGetJson(nsynjsCtx,'subscribe/url2').data;
        ...
    };
    function subscribeToThingOneThree {
        ...
        var data = ajaxGetJson(nsynjsCtx,'subscribe/url3').data;
        ...
    };
    subscribeToThingOne();
    subscribeToThingOneTwo();
    subscribeToThingOneThree();
}

第 3 步。运行 通过 nsynjs 函数形成你的 subscribeToThings 方法:

subscribeToThings() {
    this.subscribeCtx = nsynjs.run(syn_SubscribeToThings,{},function(){});
}

第 4 步。随时停止:

cleanupOnDeath () {
    this.subscribeCtx.stop();
}

它将停止执行 syn_SubscribeToThings 并自动取消所有活动的底层回调。