可以重置的延迟承诺或类似模式

Deferred promise or similar pattern that can be reset

本文关键字:模式 承诺 延迟      更新时间:2023-09-26

我知道这个问题已经被问了好几个问题,但我认为SO中的问答都没有提到这个问题中解释的场景。

我有一个JavaScript组件(它是一个KO组件,没关系)。根据此组件中的某些操作,它可以异步加载子组件的多个不同版本。这可能会发生多次。即最初它可以加载子 A,后来用子 B 替换它,依此类推。

主组件需要运行子组件的某些功能。为此,它将一个名为 registerApi 的函数传递给子组件构造函数。这样,当子组件完成加载时,它会调用 registerApi 。为了避免父组件在加载子组件之前调用 API,registerApi通过以下方式使用 promise:

var childApiDeferred = $.Deferred();
var childApiPromise = childApiDerred.promise();
var registerApi = function(api) {
  childApiDeferred.resolve(api);
};

这样,每当主组件需要调用子组件的 API 时,它都会通过 promise 来完成,这样,它只会在子组件完成加载并注册其 API 时运行。子组件 API 中的方法调用方式如下:

childApiPromise.then(function(api) { api.method(); })

目前为止,一切都好。问题是在某个时间点,子组件可以换成一个新的组件。此时,我需要将延迟重置为未解析状态,以便如果主组件尝试调用 API,它必须等到新组件加载并注册其 API。

我还没有看到任何可重置延迟的实现,也没有看到任何其他可以解决这个问题的东西。我找到的唯一解决方案,使代码更加复杂(我没有显示它),是,每当新组件开始加载时,我都会创建一个新的延迟,并公开新延迟的承诺,以便调用:

childApiPromise.then(function(api) { api.method(); })

始终参考新的应许。(正如我所说,这使代码复杂化)。

当然,对于第一个子组件,延迟/承诺就像一个魅力,但是,我正在寻找类似延迟的东西,它可以无法解析,以便每当新组件开始加载时我都可以取消解析它。

是否有一种可重置的延迟或任何其他 JavaScript 模式允许存储回调并在新功能准备就绪时调用它们?是否可以以任何方式实现它?

组件交换是如何发生的?

正如我在评论中被问到的那样,我会解释它,尽管我认为这与这个 Q 无关。

我正在使用带有组件的挖空。我有一个主组件,它有一个子组件。当我更改保存组件名称的属性时,子组件由 KO 基础结构自动交换。此时,我知道我必须"暂停"对子组件 API 的调用,因为正在加载新的子组件,而 API 目前不可用。子组件异步加载并接收一系列参数,包括来自主组件的registerApi回调。当子组件完成加载时,它会调用registerApi以向父组件公开其 API。(作为旁注,所有子组件都向主组件公开相同的功能,但实现不同)。

因此,这是交换组件时发生的步骤:

  • 主组件创建一个新的延迟"childApiDeferred",该延迟未解决
  • 主组件设置新子组件的名称,以便 ko 基础设施交换子组件,并将registerApi回调传递给子组件
  • 子组件是异步加载的,当它完成加载时,它会调用 registerApi ,这也会解决延迟的
  • 父组件可以通过 promise 安全地调用子 API,因为在解决之前不会执行它们

每次删除并创建新承诺都可以正常工作。问题在于,这需要编写大量代码来确保正确完成此操作,并且所有代码都使用新的,而不是旧的,延迟的或承诺的。如果承诺在哪里可重置 你只需要重置并解决它:两行简单的代码。

根据定义,承诺只能实现或拒绝一次。因此,您将无法重置延期承诺。我建议你的代码结构需要改变,以服从一个更 Promisey 的架构。

例如,也许主组件在子组件上调用的所有函数都应该返回 Promises。如果子项已加载,则该承诺将立即解决。像这样的东西;

class Child {
  apiMethod() {
    return this.waitForApiRegister()
      .then(() => {
        // do api stuff and return result
        return res;
      })
  },
  waitForApiRegister() {
    return ((this.apiPromise) || this.apiPromise = $.Deferred());
  }
  registerApi() {
    this.apiPromise.resolve();
  }
  unregisterApi() {
    this.apiPromise = undefined;
  }
}

然后,您可以将其称为普通承诺,因为您知道只有在注册 API 后才会解析该承诺。

const c = new Child();
c.apiMethod().then(res => {
  console.log(res);
});

如果子节点发生变化,或者出于任何原因您需要重置 Promise,您只需将其设置为 null,下次调用 api 函数时,将创建一个新的 Promise。

试一试。我没有测试过,但认为它应该有效。

function ApiAbstraction() {
    var deferred;
    this.promise = null;
    this.set = function(kill) {
        if(kill) {
            deferred.reject('this child never became active'); // This will affect the initial `deferred` and any subsequent `deferred` if not yet resolved.
        }
        deferred = $.Deferred();
        this.promise = deferred.promise();
        return deferred.resolve;
    };
    this.set(); // establish an initial (dummy) deferred (which will never be resolved).
};

首先,创建一个 ApiAbstraction 的实例:

var childAbstract = new ApiAbstraction();

对于不同类型的子项,您可以根据需要拥有任意数量的实例,但可能只需要一个实例。

然后,要加载一个新的子项(包括初始子项),让我们假设一个异步myChildLoader()方法 - 那么你有两个选择:

也:

var registerChild = childAbstract.set();
myChildLoader(params).then(registerChild);

或:

var registerChild = childAbstract.set(true);
myChildLoader(params).then(registerChild);

这两个版本都会导致任何新的 .then() 等回调立即附加到新子级,即使它尚未交付。无需信号量

kill布尔值可能很有用,但仅限于仅在尚未传递终止附加到前一个子级的任何回调。如果 is 已经交付,那么它的延迟将(很有可能)被解析,任何附加的回调要么执行,要么在执行过程中不可撤销。承诺不包括撤回附加回调的机制。

如果事实证明kill选项中没有值,则可以将其从构造函数中删除。

这很诱人,但不建议写:

myChildLoader(params).then(childAbstract.set());

这(有或没有 kill boolean)将导致任何新的 .then 处理程序附加到旧子项,直到新子项可用。但是,代码库中任何其他类似表达式都会发生危险的竞争。最后一个分娩孩子的人将赢得比赛(不一定是最后一个被召唤的人)。 ApiAbstraction()需要对进行重大修改,以确保比赛不会发生(困难?无论如何,可能是学术性的,因为您很可能想尽快切换到新的孩子。

在代码库的其他地方,您需要通过 ApiAbstraction() 实例调用子方法,该实例需要在范围内,其方式与直接调用其 API child完全相同,例如:

function doStuffInvolvingChildAPI() {
    ...
    var promise = childAbstract.promise.then(function(child) {
        child.method(...);
        return child; // make child available down the success path
    }); // ... and form a .then chain as required 
    ...
    return promise;
}

childAbstract.promise.then(...)最坏的情况是轻微的繁琐。

虽然解释很长,但这会给你"两行简单的代码",你试图交换孩子,即:

var registerChild = childAbstract.set();
myChildLoader(params).then(registerChild);

正如问题中所述,"每次删除并创建一个新承诺都可以正常工作" - 这正是.set()方法所做的。编写什么以确保所有代码都使用新的而不是旧的子代码的问题由抽象来照顾。