【问题标题】:Deferred promise or similar pattern that can be reset可以重置的延迟承诺或类似模式
【发布时间】:2016-04-19 20:51:24
【问题描述】:

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

我有一个 JavaScript 组件(它是一个 KO 组件,但没关系)。根据该组件中的某些操作,它可以异步加载子组件的几个不同版本。这可能会发生多次。 IE。本来它可以加载孩子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(); })

到目前为止,一切都很好。问题是在某个时间点,子组件可能会被一个新组件交换。此时,我需要 reset 将 deferred 设置为未解析状态,这样,如果主组件尝试调用 API,它必须等到新组件已加载并已注册它的 API。

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

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

总是指新的承诺。 (正如我所说,这会使代码复杂化)。

当然,对于第一个子组件,deferred/promise 就像一个魅力,但是,我正在寻找类似 deferred 的东西,它可以是未解决的,这样我就可以在新组件开始加载时取消它。

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

组件交换是如何发生的?

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

我将 Knockout 与组件一起使用。我有一个主组件,它有一个子组件。当我更改包含组件名称的属性时,KO 基础结构会自动交换子组件。此时我知道我必须“暂停”对子组件 API 的调用,因为正在加载一个新的子组件,而此时 API 不可用。子组件异步加载并接收一系列参数,包括来自主组件的registerApi回调。当子组件完成加载后,它会调用registerApi 将其API 暴露给父组件。 (附带说明一下,所有子组件都向主组件公开相同的功能,但实现方式不同)。

所以,这是交换组件时发生的步骤:

  • 主组件新建一个deferred“childApiDeferred”,未解决
  • 主组件设置新子组件的名称,以便ko基础架构交换子组件,并将registerApi回调传递给子组件
  • 子组件是异步加载的,加载完成后调用registerApi,也解决了deferred
  • 父组件可以通过 Promise 安全地调用子 API,因为在解决之前它们不会被执行

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

【问题讨论】:

  • 您正在寻找玻璃钢。
  • @Bergi 谢谢你的建议。我找到了开菲尔和培根。我应该检查这些还是您更喜欢任何其他选项?
  • 一切似乎都取决于“子组件可以被新组件交换”的含义。这实际上意味着什么?我们可以看看进行交换的代码吗?
  • @JotaBe:是的,我想让你看看这些,以获得一个想法。还有很多其他的,Rx.js 也很流行。我不喜欢任何特定的。
  • Knockout 已经由 observables 提供支持——您可能只想使用计算的 observable 并通过它运行更新。

标签: javascript promise deferred


【解决方案1】:

根据定义,Promise 只能实现或拒绝一次。所以你将无法重置延迟的 Promise。我建议您的代码结构需要更改以遵循更多的 Promisey 架构。

例如,也许主组件在子组件上调用的所有函数都应该返回 Promises。如果孩子已经加载,那么这个 Promise 将立即被解决。像这样的东西;

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

然后您可以将其作为普通 Promise 调用,因为您知道只有在注册 API 时才会解析 Promise。

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

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

【讨论】:

  • 这正是Isay导致复杂代码的原因:删除promise并在子组件更改时创建一个新promise。我已经实现了这样的东西,但是,因为它是真正的代码,它更适合我的情况。而且它工作正常。但是,与子组件只加载一次的情况相比,我觉得它是一种丑陋的复杂方式。 OTOH:我在子组件 API 中调用方法完全没有问题,其中一些返回承诺,而另一些则没有。问题不在于子 API,而是在加载子 API 时显示为红色的“信号量”的实现。
【解决方案2】:

试一试。我没有测试过,但认为它应该可以工作。

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 已经交付,那么它的 Deferred 将(很可能)被解决,并且任何附加的回调要么被执行,要么在执行过程中不可撤销。 Promise 不包含撤销附加回调的机制。

如果发现kill 选项中没有值,则可以将其从构造函数中删除。

很诱人但不建议写:

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

这(有或没有 kill 布尔值)将导致任何新的 .then 处理程序附加到旧的子进程,直到新的子进程可用。然而,危险的race 会出现在代码库中任何其他类似的表达式中。最后一个接生的将赢得比赛(不一定是最后一个叫的)。 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() 方法所做的。写什么以确保所有代码使用新的而不是旧的孩子的问题由抽象来处理。

【讨论】:

    猜你喜欢
    • 2012-07-08
    • 2016-12-08
    • 2014-11-25
    • 1970-01-01
    • 2016-09-22
    • 2015-03-08
    • 1970-01-01
    • 2015-03-27
    • 2023-03-21
    相关资源
    最近更新 更多