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

时间:2019-09-18 21:15:51

标签: javascript promise cancellation

假设我有一个搜索功能可以进行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已解决”

如何实现这种情况?

3 个答案:

答案 0 :(得分:2)

您可以定义一个工厂函数,以将search()方法封装为请求的取消行为。请注意,在Promise constructors are normally considered an anti-pattern期间,有必要在reject()集中保留对每个pending函数的引用,以实现早期取消。

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()的行为都是导致返回的诺言的内部状态从待处理变为已拒绝,而不是已实现稍后的诺言将首先解决,以便使用方代码将调用用于诺言解析的适当处理程序。

答案 1 :(得分:1)

类似于帕特里克·罗伯茨的回答,我建议使用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);

答案 2 :(得分:-2)

这样的承诺是不能取消的,但是在一定程度上是通过使它们被拒绝而取消的。

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