对未使用延迟[反]模式而尚未创建的承诺的承诺

时间:2016-05-25 00:33:53

标签: javascript promise deferred

问题1:在给定时间只允许一个API请求,因此真实的网络请求排队,而尚未完成的请求。应用程序可以随时调用API级别并期望获得回报。当API调用排队时,将在未来的某个时刻创建网络请求的承诺 - 返回应用程序的内容是什么?这就是如何用延迟的“代理”承诺解决它:

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的应用程序期待作为回报的承诺。

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

由于延迟被认为是一种反模式,(另见When would someone need to create a deferred?),问题是 - 是否有可能在没有延迟(或类似new Promise(function (resolve, reject) {outerVar = [resolve, reject]});的等效黑客)的情况下实现相同的事情,使用标准的Promise API?

3 个答案:

答案 0 :(得分:4)

  

承诺尚未创造的承诺

...通过使用回调链接then调用很容易构建,该回调创建了对promise的承诺,表示将来创建它的可用性。

如果您承诺承诺,则永远不会使用延迟模式。当且仅当存在您想要等待的异步时,您应该使用延迟或Promise构造函数,并且它还没有涉及promises 。在所有其他情况下,您应该撰写多个承诺。

当你说

  

当API调用排队时,将在未来的某个时间点创建网络请求的承诺

那么你不应该创建一个延迟,你可以在以后用承诺解决它(或者更糟糕的是,一旦承诺结算后用承诺结果解决它),但你应该得到一个承诺。将来的网络需求。基本上你要写

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

这具有额外的好处,您需要显式处理队列中的错误。在这里,每当一个请求失败时,每个调用都会返回一个被拒绝的承诺(您可能想要更改它) - 在您的原始代码中,queue刚被卡住(您可能没有注意到)。

第二种情况有点复杂,因为它涉及setTimeout调用。这是一个异步原语,我们需要手动构建一个承诺 - 但仅限于超时,而不是其他任何东西。同样,我们将获得超时的承诺,然后简单地将我们的API调用链接到那个以获得我们想要返回的承诺。

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构造函数如何只关注超时和没有其他的。它甚至不需要像延迟那样“泄漏”resolve函数,它唯一可用于外部的是renew函数,它允许扩展计时器。

答案 1 :(得分:3)

  

当API调用排队时,网络请求的承诺就会出现   是在未来的某个时刻创建的 - 返回应用程序的内容是什么?

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

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()创建新承诺,以便您可以立即返回该承诺 - 将来无需创建承诺。通过在doRealNetRequest()处理程序中返回其值,对.then()的实际调用将链接到此反复的.then()承诺。这是有效的,因为我们提供给.then()的回调直到链中的先前承诺解决后才会被调用,这给我们一个自动触发器来执行前一个链中的下一个饰面。

此实现假定您希望排队的API调用即使在返回错误后也能继续。围绕handle rejection注释的额外几行代码可以确保即使先前的承诺拒绝,链也会继续。任何拒绝都会按预期返回给调用者。

这是你的第二个解决方案(你称之为去抖动)。

  

问题是 - 是否有可能在没有a的情况下实现相同的目标   推迟(或类似新的承诺的黑客(功能(解决,   拒绝){outerVar = [resolve,reject]});),使用标准的Promise   API?

据我所知,debouncer类型的问题需要一点点黑客来揭示从promise执行者之外以某种方式触发解析/拒绝回调的能力。通过公开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;
    }
})();

答案 2 :(得分:0)

您可以创建一个队列,该队列按队列

中的顺序解析承诺

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>