未使用延迟[反]模式的情况下尚未创建的承诺的承诺

Promises for promises that are yet to be created without using the deferred [anti]pattern

问题1:在给定时间内只允许一个API请求,所以真正的网络请求在排队,而还有一个还没有完成。应用程序可以随时调用 API 级别并期待 return 中的承诺。当 API 调用排队时,网络请求的承诺将在未来的某个时间点创建 - return 对应用程序做什么?这就是用延迟 "proxy" promise 解决问题的方法:

var queue = [];
function callAPI (params) {
  if (API_available) {
    API_available = false;
    return doRealNetRequest(params).then(function(data){
      API_available = true;
      continueRequests();
      return data;
    });
  } else {
    var deferred = Promise.defer();
    function makeRequest() {
      API_available = false;
      doRealNetRequest(params).then(function(data) {
        deferred.resolve(data);
        API_available = true;
        continueRequests();
      }, deferred.reject);
    }
    queue.push(makeRequest);
    return deferred.promise;
  }
}

function continueRequests() {
  if (queue.length) {
    var makeRequest = queue.shift();
    makeRequest();
  }
}

问题2:部分API调用去抖动,使待发送的数据随着时间累积,超时后批量发送。调用 API 的应用期望在 return 中得到一个承诺。

var queue = null;
var timeout = 0;
function callAPI2(data) {
  if (!queue) {
    queue = {data: [], deferred: Promise.defer()};
  }
  queue.data.push(data);
  clearTimeout(timeout);
  timeout = setTimeout(processData, 10);
  return queue.deferred.promise;
}

function processData() {
  callAPI(queue.data).then(queue.deferred.resolve, queue.deferred.reject);
  queue = null;
}

由于 deferred 被认为是一种反模式,(另请参阅 ),问题是 - 是否有可能在没有 deferred 的情况下实现相同的事情(或类似 new Promise(function (resolve, reject) {outerVar = [resolve, reject]}); 的等价 hack) , 使用标准的 Promise API?

Promises for promises that are yet to be created

…通过将 then 调用与创建承诺的回调链接起来很容易构建,承诺代表将来创建它的可用性。

如果您要为承诺做出承诺,您应该永远不要使用延迟模式。你应该使用 deferreds 或 Promise 构造函数,当且仅当你想要等待一些异步的东西,并且 它还没有涉及承诺 。在所有其他情况下,您应该编写多个承诺。

当你说

When the API call is queued, the promise for the network request would be created at some point in the future

那么你不应该创建一个延迟,你可以在创建后用承诺解决(或者更糟,一旦承诺解决就用承诺结果解决),而是你应该得到一个承诺将来会发出网络请求。基本上你要写

return waitForEndOfQueue().then(makeNetworkRequest);

当然我们需要分别改变队列。

var queue_ready = Promise.resolve(true);
function callAPI(params) {
  var result = queue_ready.then(function(API_available) {
    return doRealNetRequest(params);
  });
  queue_ready = result.then(function() {
    return true;
  });
  return result;
}

这还有一个额外的好处,就是您需要明确处理队列中的错误。在这里,一旦一个请求失败,每次调用 return 都是一个被拒绝的承诺(您可能想要更改它)- 在您的原始代码中,queue 只是卡住了(您可能没有注意到) .

第二种情况有点复杂,因为它确实涉及 setTimeout 调用。这是一个异步原语,我们需要手动为其构建一个承诺——但仅针对超时,除此之外别无其他。同样,我们将获得超时承诺,然后简单地将我们的 API 调用链接到它以获得我们想要 return.

的承诺
function TimeoutQueue(timeout) {
  var data = [], timer = 0;
  this.promise = new Promise(resolve => {
    this.renew = () => {
      clearTimeout(timer);
      timer = setTimeout(resolve, timeout);
    };
  }).then(() => {
    this.constructor(timeout); // re-initialise
    return data;
  });
  this.add = (datum) => {
    data.push(datum);
    this.renew();
    return this.promise;
  };
}

var queue = new TimeoutQueue(10);
function callAPI2(data) {
  return queue.add(data).then(callAPI);
}

你可以在这里看到 a) 去抖动逻辑是如何完全从 callAPI2 中分解出来的(这可能不是必需的,但提出了一个很好的观点)和 b) promise 构造函数如何只关注它自己超时,仅此而已。它甚至不需要像 deferred 那样 "leak" resolve 函数,它对外部唯一可用的是允许扩展计时器的 renew 函数。

When the API call is queued, the promise for the network request would be created at some point in the future - what to return to the app?

你的第一个问题可以通过 promise 链来解决。在所有先前的请求都完成之前,您不想执行给定的请求,并且您希望按顺序连续执行它们。这正是 promise 链的设计模式。你可以这样解决:

var callAPI = (function() {
    var p = Promise.resolve();
    return function(params) {
        // construct a promise that chains to prior callAPI promises
        var returnP = p.then(function() {
            return doRealNetRequest(params);
        });
        // make sure the promise we are chaining to does not abort chaining on errors
        p = returnP.then(null, function(err) {
            // handle rejection locally for purposes of continuing chaining
            return;
        });
        // return the new promise
        return returnP;
    }
})();

在此解决方案中,实际上会立即使用 .then() 创建新的承诺,因此您可以立即 return 该承诺 - 以后无需创建承诺。对 doRealNetRequest() 的实际调用通过 return 其在 .then() 处理程序中的值链接到此返回的 .then() 承诺。这是有效的,因为我们提供给 .then() 的回调直到未来某个时间才会被调用,当链中的先前承诺已经解决时,给我们一个自动触发器来执行链中的下一个,而前一个承诺完成。

此实现假设您希望排队的 API 调用在一个 return 错误后继续。 handle rejection 注释周围的额外几行代码用于确保即使先前的 promise 被拒绝,链也会继续。任何拒绝都会按预期 return 返回给调用者。


这是第二个问题的解决方案(所谓的去抖动)。

the question is - is it possible to achieve the same things without a deferred (or equivalent hacks like new Promise(function (resolve, reject) {outerVar = [resolve, reject]});), using the standard Promise API?

据我所知,去抖动器类型的问题需要一点技巧才能暴露从 promise 执行器外部以某种方式触发 resolve/reject 回调的能力。通过公开 promise 执行程序函数中的单个函数而不是直接公开 resolve 和 reject 处理程序,它可以比您建议的更简洁。

此解决方案创建了一个闭包来存储私有状态,可用于管理从一次调用到 callAPI2() 到下一次调用的事情。

为了允许未来不确定时间的代码触发最终解决方案,这会在 promise 执行器函数中创建一个本地函数(它可以访问 resolvereject 函数)然后将其共享到更高(但仍然是私有)范围,以便可以从 promise 执行程序函数外部调用它,但不能从 callAPI2.

外部调用
var callAPI2 = (function() {
    var p, timer, trigger, queue = [];
    return function(data) {
        if (!p) {
            p = new Promise(function(resolve) {
                // share completion function to a higher scope
                trigger = function() {
                    resolve(queue);
                    // reinitialize for future calls
                    p = null;
                    queue = [];
                }
            }).then(callAPI);
        }
        // save data and reset timer
        queue.push(data);
        clearTimeout(timer);
        setTimeout(trigger, 10);
        return p;
    }
})();

您可以创建一个队列,它按照放入队列的顺序解析承诺

window.onload = function() {
  (function(window) {
    window.dfd = {};
    that = window.dfd;
    that.queue = queue;

    function queue(message, speed, callback, done) {

      if (!this.hasOwnProperty("_queue")) {
        this._queue = [];
        this.done = [];
        this.res = [];
        this.complete = false;
        this.count = -1;
      };
      q = this._queue,
        msgs = this.res;
      var arr = Array.prototype.concat.apply([], arguments);
      q.push(arr);
      msgs.push(message);
      var fn = function(m, s, cb, d) {

        var j = this;
        if (cb) {
          j.callback = cb;
        }
        if (d) {
          j.done.push([d, j._queue.length])
        }
        // alternatively `Promise.resolve(j)`, `j` : `dfd` object
        // `Promise` constructor not necessary here,
        // included to demonstrate asynchronous processing or
        // returned results
        return new Promise(function(resolve, reject) {
            // do stuff
            setTimeout(function() {
              div.innerHTML += m + "<br>";
              resolve(j)
            }, s || 0)
          })
          // call `cb` here, interrupting queue
          .then(cb ? j.callback.bind(j, j._queue.length) : j)
          .then(function(el) {
            console.log("queue.length:", q.length, "complete:", el.complete);
            if (q.length > 1) {
              q.splice(0, 1);
              fn.apply(el, q[0]);
              return el
            } else {
              el._queue = [];
              console.log("queue.length:", el._queue.length
                          , "complete:", (el.complete = !el._queue.length));
              always(promise(el), ["complete", msgs])
            };
            return el
          });
        return j
      }

      , promise = function(t) {
        ++t.count;
        var len = t._queue.length,
          pending = len + " pending";
        return Promise.resolve(
          len === 1 
          ? fn.apply(t, t._queue[0]) && pending 
          : !(t.complete = len === 0) ? pending : t
        )
      }

      , always = function(elem, args) {
        if (args[0] === "start") {
          console.log(elem, args[0]);
        } else {
          elem.then(function(_completeQueue) {
            console.log(_completeQueue, args);
              // call any `done` callbacks passed as parameter to `.queue()`
              Promise.all(_completeQueue.done.map(function(d) {
                return d[0].call(_completeQueue, d[1])
              }))
              .then(function() {
                console.log(JSON.stringify(_completeQueue.res, null, 2))
              })
          })
        }
      };

      always(promise(this), ["start", message, q.length]);
      return window
    };
  }(window));

  window
    .dfd.queue("chain", 1000)
    .dfd.queue("a", 1000)
    .dfd.queue("b", 2000)
    .dfd.queue("c", 2000, function callback(n) {
      console.log("callback at queue index ", n, this);
      return this
    }, function done(n) {
      console.log("all done callback attached at queue index " + n)
    })
    .dfd.queue("do", 2000)
    .dfd.queue("other", 2000)
    .dfd.queue("stuff", 2000);

  for (var i = 0; i < 10; i++) {
    window.dfd.queue(i, 1000)
  };

  window.dfd.queue.apply(window.dfd, ["test 1", 5000]);
  window.dfd.queue(["test 2", 1000]);

  var div = document.getElementsByTagName("div")[0];
  var input = document.querySelector("input");
  var button = document.querySelector("button");

  button.onclick = function() {
    window.dfd.queue(input.value, 0);
    input.value = "";
  }
}
<input type="text" />
<button>add message</button>
<br>
<div></div>