对尚未使用延迟 [反] 模式创建的 Promise 的 Promise

IT技术 javascript promise deferred
2021-01-16 10:17:18

问题 1:在给定时间只允许一个 API 请求,因此真正的网络请求在排队,而有一个尚未完成。应用程序可以随时调用 API 级别并期望得到Promise的回报。当 API 调用排队时,网络请求的Promise将在未来的某个时刻创建 - 返回给应用程序的内容是什么?这就是如何通过延迟的“代理”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();
  }
}

问题二:部分API调用debounce,使得待发送的数据随着时间的推移积累起来,到了超时就批量发送。调用 API 的应用程序期望得到Promise作为回报。

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

由于延迟被认为是一种反模式,(另请参阅何时有人需要创建延迟?),问题是 - 是否可以new Promise(function (resolve, reject) {outerVar = [resolve, reject]});使用标准的 Promise API 在没有延迟(或类似的 hacks )的情况下实现相同的事情?

3个回答

尚未创建的Promise的Promise

...通过将then调用与创建Promise的回调链接起来很容易构建,Promise表示将来创建它的可用性。

如果您要为Promise做出Promise,则永远不应使用延迟模式。Promise当且仅当您想要等待一些异步操作,并且它尚未涉及 promises 时,您才应该使用 deferreds 或构造函数在所有其他情况下,您应该组合多个Promise。

当你说

当 API 调用排队时,网络请求的Promise将在未来的某个时刻创建

那么你不应该创建一个延迟,你可以在创建后用Promise解决(或者更糟的是,一旦Promise解决,就用Promise结果解决它),而是你应该得到一个未来点的Promise将进行网络请求。基本上你要写

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

这有额外的好处,您需要明确处理队列中的错误。在这里,一旦一个请求失败,每个调用都会返回一个被拒绝的Promise(你可能想要改变它)——在你的原始代码中,queue刚刚卡住了(你可能没有注意到)。

第二种情况有点复杂,因为它确实涉及setTimeout调用。这是一个异步原语,我们需要为它手动构建一个Promise——但仅限于超时,没有别的。同样,我们将获得超时的Promise,然后简单地将我们的 API 调用链接到它以获得我们想要返回的Promise。

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允许扩展计时器的函数。

@Bergi 感谢您的回答!因此,您基本上从第一个(即等待队列)的解析处理程序中返回第二个Promise,并且该特定Promise将then在外部代码中处理,对吗?
2021-03-17 10:17:18
@mderk:是的,那是如何then工作:-)
2021-03-17 10:17:18
另外,您调用resolve时没有任何参数,所以我看不到您的.then()处理程序如何获得data您所指望参数。
2021-03-29 10:17:18
@jfriend00:谢谢,那个data参数确实需要删除,data应该通过闭包来引用数组。我已经setTimeout在我摆弄过的代码的先前版本中将数组传递给了。也许我对这TimeoutQueue件事有点过头了:-)
2021-04-02 10:17:18
@jfriend00:这里没有需要解决的“事先Promise”,每次都add()返回相同的内容.promise(直到超时实际到期,当Promise解决并创建一个新的.promise.add方法时)。timer被替换为一个新的调用相同resolve,但在每一个到期稍后add调用。
2021-04-11 10:17:18

当 API 调用排队时,网络请求的Promise将在未来的某个时刻创建 - 返回给应用程序的内容是什么?

您的第一个问题可以通过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;
    }
})();

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

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


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

问题是 - 是否有可能使用标准的 Promise API 在没有延迟(或类似 new Promise(function (resolve, reject) {outerVar = [resolve, reject]});)的情况下实现相同的事情?

据我所知,debouncer 类型的问题需要一点点技巧来暴露以某种方式从Promise执行器外部触发解决/拒绝回调的能力。通过公开Promise执行器函数内的单个函数而不是直接公开解析和拒绝处理程序,它可以比您建议的更简洁。

该解决方案创建了一个闭包来存储私有状态,该状态可用于管理从一个调用到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;
    }
})();
是的,您没有忘记reject作为第二个参数传递正确的,但是每次我看到.then(resolve …我都会不寒而栗,因为其他人会采用这种模式并最终忘记错误。最好不要使用该模式:-)
2021-03-18 10:17:18
@Bergi - 谢谢。我采纳了你的第一个建议。
2021-03-24 10:17:18
@Bergi - 是的,我已经想通了并再次更改了它。
2021-04-01 10:17:18
您可以(并且 imo应该)将其简化resolve(callAPI(queue))为至少避免Promise构造函数反模式的一些危险或者最好立即callAPI开始then通话。
2021-04-02 10:17:18
顺便说一句,您的旧代码和当前代码仍然容易受到callAPI调用本身引发的异常的影响
2021-04-04 10:17:18

您可以创建一个队列,该队列按照放入队列的顺序解析Promise

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>