如果最后一个Promise还没有resolved,如何取消它?

4

假设我有一个搜索函数来进行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 链接在一起,当其中一个被拒绝时,链条将停止。如果您没有将它们链接在一起,它们将每次都执行。 - chevybow
其实一次只处理一个承诺应该就可以工作 :/ - Washington Guedes
@Washington 如果操作员一次只处理一个,他怎么知道哪个是最慢的(需要2000毫秒的调用)?看起来他想让它们全部竞赛,并手动杀死最后一个承诺马,而不给它自己死亡或完成比赛的机会。 - TheMaster
@TheMaster 你说得对,我没能编写处理一个接一个的 Promise 的代码。 - Washington Guedes
2
你的问题存在歧义。如果真的当一个新请求到来时需要取消最后一个 promise,那么你的代码示例应该输出“search1 rejected”,而不是“search1 resolved”,因为第二次调用 search 时,应该将之前的 promise 标记为未解决。但是,如果你想要优先选择哪一个 promise 先解决,那么输出应该是“search1 resolved”,而其他两个则被拒绝。请明确你的问题逻辑,并使代码与解释保持一致。 - trincot
显示剩余12条评论
3个回答

3

承诺本质上是不可取消的,但可以通过使它们被拒绝来在有限的意义上取消。

考虑到这一点,可以通过围绕 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_'以区分取消和其他拒绝原因。


我不知道你是怎么得出那个理解的。这毫无意义。 - Roamer-1888
好的,那么让我们只看看明确说明的内容。他们要求“查看search1已解决search2已拒绝search3已解决”。你的解决方案并没有做到这一点。 - Patrick Roberts
我同意,但它确实回答了标题问题“如果没有解决,如何取消最后一个Promise?” - Roamer-1888
@trincot,这个问题不够明确。这是标题问题和开头段落的解决方案。OP的“需要看到search1已解决search2被拒绝search3已解决”与这两者都不一致。他需要澄清。如果我错了,我会很乐意删除。 - Roamer-1888
2
确实,这是模棱两可的。 - trincot
显示剩余11条评论

2

您可以定义一个工厂函数来封装您的search()方法,并具有请求取消功能。请注意,虽然Promise构造函数通常被认为是反模式,但在这种情况下,有必要保留对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的插入顺序迭代,仅取消在返回已经 settled promise 的调用之前所做的调用返回的待定 promise。


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

cancellation() 的作用只是使返回的 promise 的内部状态从 pending 转换为 rejected,而不是 fulfilled,如果后续的 promise 先 settled,那么消费代码将调用适当的处理程序来处理 promise resolution。


1
与PatrickRoberts的答案类似,我建议使用Map来维护待处理的promise列表。
但是,我不会在promise构造函数外部保留对reject回调的引用。我建议放弃“拒绝”过时的promise的想法。相反,只需忽略它。将其包装在一个永远不会解决或拒绝的promise中,但仅保持为死亡promise对象,不会更改状态。实际上,这个静默的promise可以是每次需要它的情况下都相同的一个。
以下是实现方法:

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 { ... } 释放资源的最终阶段中,这样做会导致资源泄漏。因此,我认为 Promise 应该始终 settle,即使你必须区分由于异常或取消而导致的拒绝。 - Patrick Roberts
@PatrickRoberts,当然,这就像说“如果我的代码期望发生A,我应该让A发生”。如果您的代码需要承诺解决一些清理工作,那么您应该...解决它们。但是承诺只是对象,所以您可以像处理其他对象一样处理它们。如何释放资源本质上不是与承诺相关的主题。但是,如果您设计的代码假定承诺始终已解决,则应保持一致。我当然不会争论这一点。另请参见此答案 - trincot
感谢您深入的回答。我查看了您提供的答案,但仍然对“如何处理释放资源本质上不是与 Promise 相关的话题”这一观点感到有些困惑。如果不考虑 Promise 作为非阻塞操作完成信号的事实,我可能会倾向于同意这一观点。但是,Promise 已经深度集成到语言中,其唯一目的是用于标记非阻塞操作的完成。如果从方程式中删除完成信号,除非使用 WeakRef,否则将无法清理手动分配的资源。 - Patrick Roberts
顺便说一下,我并不讨厌你的回答(事实上,你得到的赞同来自于我)。我只是想确保我清楚地理解了这个设计选择,即保持未解决状态而不是拒绝取消。 - Patrick Roberts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接