如何等待动态承诺列表中的最后一个承诺?

How to wait for the last promise in a dynamic list of promises?

我有一个启动异步进程 X 的函数 F。函数 returns 一个在 X 结束时解决的承诺(我通过 X 返回的承诺学习)。

虽然 (w.l.o.g.) X 的第一个实例 X1 是 运行,但可能会有更多对 F 的调用。每个调用都会产生一个新的 X 实例,例如X2、X3 等等。

现在,困难在于: 当创建 X2 时,根据 X1 的状态,X1 应该结束或中止。只有当 X1 不再处于活动状态时,X2 才应该开始工作。 在任何情况下,所有 之前对 F 的调用返回的未解决的承诺应该在 X2 结束后 解决,以及- 或者,X 的任何后续实例,如果在 X2 为 运行.

时再次调用 F

到目前为止,对 F 的第一次调用调用 $q.defer() 来创建一个 deferred,其 promise 由所有对 F 的调用返回,直到最后一个 X 结束。 (然后,deferred 应该被 resolved 并且保存它的字段应该被重置为 null,等待下一个集群调用 F。)

现在,我的问题是等到 X 的所有实例都完成。我知道如果我事先有 X 实例的完整列表,我可以使用 $q.all,但由于我必须考虑以后对 F 的调用,这不是这里的解决方案。理想情况下,我可能应该 then 将某些东西链接到 X 返回的承诺以解决延迟,并且 "unchain" 一旦我将它链接到 X 的后续实例,它就会起作用。

我想是这样的:

var currentDeferred = null;

function F() {
    if (!currentDeferred) {
        currentDeferred = $q.defer();
    }

    // if previous X then "unchain" its promise handler
    X().then(function () {
        var curDef = currentDeferred;
        currentDeferred = null;
        curDef.resolve();
    });

    return currentDeferred.promise;
}

但是,我不知道如何执行 "unchaining",如果这是正确的解决方案。

我该怎么做?我是否遗漏了 promise 的某些常见模式甚至内置功能,或者我完全走错了路?


添加一点上下文:调用 F 来加载数据(异步)并更新一些视觉输出。 F returns 只有在视觉输出再次更新到稳定状态(即没有更多更新挂起)后才应解决的承诺。

F is called to load data (asynchronously) and updating some visual output. F returns a promise that should only be resolved once the visual output is updated to a stable state again (i.e. with no more updates pending).

由于 F 的所有调用者都将收到他们需要消费的承诺,但您只想在所有堆叠调用完成后更新 UI,最简单的事情是让每个承诺如果有另一个 "get more data" 呼叫挂起,则用一个值告诉调用者不要更新 UI 来解析(或拒绝);这样,只有 promise 最后解决的调用者才会更新 UI。您可以通过跟踪未完成的呼叫来做到这一点:

let accumulator = [];
let outstanding = 0;
function F(val) {
  ++outstanding;
  return getData(val)
    .then(data => {
      accumulator.push(data);
      return --outstanding == 0 ? accumulator.slice() : null;
    })
    .catch(error => {
      --outstanding;
      throw error;
    });
}

// Fake data load
function getData(val) {
  return new Promise(resolve => {
    setTimeout(resolve, Math.random() * 500, "data for " + val);
  });
}

let accumulator = [];
let outstanding = 0;
function F(val) {
  ++outstanding;
  return getData(val)
    .then(data => {
      accumulator.push(data);
      return --outstanding == 0 ? accumulator.slice() : null;
    })
    .catch(error => {
      --outstanding;
      throw error;
    });
}

// Resolution and rejection handlers for our test calls below
const resolved = data => {
  console.log("chain done:", data ? ("update: " + data.join(", ")) : "don't update");
};
const rejected = error => { // This never gets called, we don't reject
  console.error(error);
};

// A single call:
F("a").then(resolved).catch(rejected);

setTimeout(() => {
  // One subsequent call
  console.log("----");
  F("b1").then(resolved).catch(rejected);
  F("b2").then(resolved).catch(rejected);
}, 600);

setTimeout(() => {
  // Two subsequent calls
  console.log("----");
  F("c1").then(resolved).catch(rejected);
  F("c2").then(resolved).catch(rejected);
  F("c3").then(resolved).catch(rejected);
}, 1200);
.as-console-wrapper {
  max-height: 100% !important;
}

(这是原生承诺;根据需要为 $q 进行调整。)

对我来说,"don't update" 不同于 "failed,",所以我使用标志值 (null) 而不是拒绝来表示它。但当然,您也可以使用带有标志值的拒绝,这取决于您。 (这将有利于将条件逻辑 [“这是一个真正的错误还是只是一个 "don't update"?] 在你的 catch 处理程序而不是你的 then [这是真实的数据还是不?]...嗯,我现在想起来可能会走另一条路。但那是微不足道的改变。)

显然,上面的 accumulator 只是您真实数据结构的粗略占位符(它不会尝试按照请求的顺序保存接收到的数据)。

我正在通过 copy 上述数据 (accumulator.slice()) 解决承诺,但在您的情况下可能没有必要。