如果没有解决,如何取消最后的承诺?

How to cancel last Promise if not resolved?

假设我有一个进行 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 resolved" "search2 rejected" "search3 resolved"

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

您可以定义一个工厂函数来封装您的 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 的插入顺序迭代仅取消在调用返回刚刚确定的承诺之前进行的调用所返回的未决承诺。


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

所有 cancellation() 所做的是导致返回的承诺的内部状态从 pending 转换为 rejected 而不是 fulfilled 如果稍后的 promise 首先解决,以便使用代码调用 promise 解析的适当处理程序。

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 可执行文件都会被调用以进行 "cancel" 先前的搜索(如果它存在并且其竞争尚未完成)。

// 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_' 以区分取消和其他拒绝原因。

与 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);