【问题标题】:How to cancel last Promise if not resolved?如果未解决,如何取消最后一个 Promise?
【发布时间】:2019-09-18 21:15:51
【问题描述】:

假设我有一个搜索功能来进行 HTTP 调用。 每次通话可能需要不同的时间。 所以我需要取消最后一个HTTP请求,只等待最后一个调用

async function search(timeout){

   const data = await promise(timeout)
   return data;

}
// the promise function is only for visualizing an http call
function promise(timeout){
   return new Promise(resolve,reject){
       setTimeout(function(){      
           resolve()
       },timeout) 
   }
}
search(200)
.then(function(){console.log('search1 resolved')})
.catch(function() {console.log('search1 rejected')})
search(2000)
.then(function(){console.log('search2 resolved')})
.catch(function(){console.log('search2 rejected')})
search(1000)
.then(function(){console.log('search3 resolved')})
.catch(function(){console.log('search3 rejected')})

需要查看“search1 已解决”“search2 已拒绝”“search3 已解决”

我怎样才能实现这个场景?

【问题讨论】:

  • 如果你有多个 Promise 链接在一起 - 当其中一个被拒绝时链将停止。您没有将它们链接起来,因此它们每次都会执行。
  • Aksi bit rekated ti question,但你应该转向 async await 它更简单
  • 这不是我想要的场景。如果尚未解决 promise 函数,我只需要取消最后一次调用 promise 函数只需可视化一个异步调用,如 API 我只想解析最后一个 promise 或在 1000 ms 之前解决的任何其他 promise
  • 你的Promise结构应该是new Promise((resolve, reject) => ...),还有,记录promise2 resolved有什么问题?
  • 您的问题含糊不清。如果当有新请求进入时确实应该取消最后一个承诺,那么您的代码示例应该产生“search1 denied”,而不是“search1 resolved”,因为search 的第二次调用应该将先前的承诺标识为未解决。但是,如果您想优先考虑最先解决的承诺,则输出应为“search1 resolved”,而其他两个被拒绝。请澄清您问题中的逻辑,并使代码与该解释保持一致。

标签: javascript promise cancellation


【解决方案1】:

您可以定义一个工厂函数来封装您的 search() 方法与请求的取消行为。请注意,虽然Promise constructors are normally considered an anti-pattern,但在这种情况下,必须保留对pending 集合中每个reject() 函数的引用,以实现提前取消。

function cancellable(fn) {
  const pending = new Set();

  return function() {
    return new Promise(async (resolve, reject) => {
      let settle;
      let result;

      try {
        pending.add(reject);
        settle = resolve;
        result = await Promise.resolve(fn.apply(this, arguments));
      } catch (error) {
        settle = reject;
        result = error;
      }

      // if this promise has not been cancelled
      if (pending.has(reject)) {
        // cancel the pending promises from calls made before this
        for (const cancel of pending) {
          pending.delete(cancel);

          if (cancel !== reject) {
            cancel();
          } else {
            break;
          }
        }

        settle(result);
      }
    });
  };
}

// internal API function
function searchImpl(timeout) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, timeout);
  });
}

// pass your internal API function to cancellable()
// and use the return value as the external API function
const search = cancellable(searchImpl);

search(200).then(() => {
  console.log('search1 resolved');
}, () => {
  console.log('search1 rejected');
});

search(2000).then(() => {
  console.log('search2 resolved');
}, () => {
  console.log('search2 rejected');
});

search(1000).then(() => {
  console.log('search3 resolved');
}, () => {
  console.log('search3 rejected');
});

search(500).then(function() {
  console.log('search4 resolved');
}, () => {
  console.log('search4 rejected');
});

这个工厂函数利用Set 的插入顺序迭代来仅取消在返回刚刚解决的promise 的调用之前发出的调用返回的未决promise。


请注意,使用reject() 取消承诺不会终止任何由创建承诺所启动的底层异步进程。每个 HTTP 请求将继续完成,以及在承诺解决之前在 search() 中调用的任何其他内部处理程序。

cancellation() 所做的只是导致返回的 Promise 的内部状态从 pending 转换为 rejected 而不是 fulfilled承诺首先解决,以便消费代码调用用于承诺解决的适当处理程序。

【讨论】:

    【解决方案2】:

    与 PatrickRoberts 的回答类似,我建议使用 Map 来维护待处理的承诺列表。

    但是,我不会在 Promise 构造函数之外维护对 reject 回调的引用。我建议放弃拒绝过时的承诺的想法。相反,忽略它。将它包装在一个永远不会解决或拒绝的承诺中,但它只是一个永远不会改变状态的死承诺对象。事实上,对于您需要的每一种情况,这种无声的承诺都可能是同一个承诺。

    这可能是这样的:

    const delay = (timeout, data) => new Promise(resolve => setTimeout(() => resolve(data), timeout));
    const godot = new Promise(() => null);
    
    const search = (function () { // closure...
        const requests = new Map; // ... so to have shared variables
        let id = 1;
        
        return async function search() {
            let duration = Math.floor(Math.random() * 2000);
            let request = delay(duration, "data" + id); // This would be an HTTP request
            requests.set(request, id++);
            let data = await request;
            if (!requests.has(request)) return godot; // Never resolve...
            for (let [pendingRequest, pendingId] of requests) {
                if (pendingRequest === request) break;
                requests.delete(pendingRequest);
                // Just for demo we output something. 
                // Not needed in a real scenario:
                console.log("ignoring search " + pendingId);
            }
            requests.delete(request);
            return data;
        }    
    })();
    
    const reportSuccess = data => console.log("search resolved with " + data);
    const reportError = err => console.log('search rejected with ' + err);
    
    // Generate a search at regular intervals.
    // In a real scenario this could happen in response to key events.
    // Each promise resolves with a random duration.
    setInterval(() => search().then(reportSuccess).catch(reportError), 100);

    【讨论】:

    • 我知道是什么让我对这个前提感到不安。虽然我同意在某些情况下您不希望调用.then() .catch(),但在某些情况下,这会泄漏通过.finally()try { await ... } finally { ... } 在最终确定期间释放的资源。出于这个原因,我认为承诺应该始终解决,即使您必须区分由于异常或取消而被拒绝。
    • @PatrickRoberts,当然,这就像说 “如果我的代码期望 A 发生,我应该让 A 发生”。如果您的代码需要承诺进行一些清理工作,您应该......解决它们。但是 Promise 只是对象,所以你可以这样处理它们。你如何处理释放资源本质上不是一个与 Promise 相关的话题。但是同样,如果您以假设承诺始终得到解决的方式设计代码,那么您应该与该选择保持一致。当然,我不是在争论这一点。另请参阅this answer
    • 感谢您富有洞察力的回复。我看了你链接的答案,但我仍然对“你如何处理释放资源本质上不是与承诺相关的话题”的概念感到有些困惑。如果不是因为 Promise 被深度集成到语言中,其唯一目的是发出非阻塞操作的完成信号,我可能会倾向于同意这一点。如果您从等式中删除完成信号,则除非您使用WeakRef,否则您将无法清理手动分配的资源。
    • 顺便说一句,我不喜欢你的回答(事实上你的支持来自我)。我只是想确保我清楚地理解设计选择是保持不安而不是拒绝取消。
    【解决方案3】:

    Promise 本身不可取消,但在有限的意义上通过导致它们被拒绝而被取消。

    考虑到这一点,可以通过围绕Promise.race() 和您希望取消的承诺返回函数进行少量详细说明来实现取消。

    function makeCancellable(fn) {
        var reject_; // cache for the latest `reject` executable
        return function(...params) {
            if(reject_) reject_(new Error('_cancelled_')); // If previous reject_ exists, cancel it.
                                                           // Note, this has an effect only if the previous race is still pending.
            let canceller = new Promise((resolve, reject) => { // create canceller promise
                reject_ = reject; // cache the canceller's `reject` executable
            });
            return Promise.race([canceller, fn.apply(null, params)]); // now race the promise of interest against the canceller
        }
    }
    

    假设您的 http 调用函数名为 httpRequestpromise 令人困惑):

    const search = makeCancellable(httpRequest);
    

    现在,每次调用 search() 时,都会调用缓存的 reject 可执行文件以“取消”前面的搜索(如果它存在并且它的竞争尚未完成)。

    // Search 1: straightforward - nothing to cancel - httpRequest(200) is called
    search(200)
    .then(function() { console.log('search1 resolved') })
    .catch(function(err) { console.log('search3 rejected', err) });
    
    // Search 2: search 1 is cancelled and its catch callback fires - httpRequest(2000) is called
    search(2000)
    .then(function() { console.log('search2 resolved') })
    .catch(function(err) { console.log('search3 rejected', err) });
    
    // Search 3: search 2 is cancelled and its catch callback fires - httpRequest(1000) is called
    search(1000)
    .then(function() { console.log('search3 resolved') })
    .catch(function(err) { console.log('search3 rejected', err) });
    

    如有必要,catch 回调可以测试err.message === '_cancelled_' 以区分取消和其他拒绝原因。

    【讨论】:

    • 您的代码没有产生请求的行为。使用您的函数,search(2000) 会导致 search(200) 在调用 search(2000)立即取消。请求的行为是仅当第二个promise 在第一个promise 之前完成时才取消。请注意,请求是让search(200) 解决,而不是拒绝,因为它在search(2000) 之前解决。
    • 我不知道你是如何理解的。这没有任何意义。
    • 好吧,让我们看看当时明确说明了什么。他们要求to see search1 resolved search2 rejected search3 resolved。您的解决方案没有这样做。
    • 我同意,但它确实回答了标题问题“如果未解决[d],如何取消最后一个承诺?”
    • 确实是模棱两可。
    猜你喜欢
    • 1970-01-01
    • 2020-10-06
    • 2016-01-20
    • 2021-04-15
    • 2018-07-16
    • 1970-01-01
    • 2021-07-10
    • 2018-01-24
    • 2021-10-29
    相关资源
    最近更新 更多