如果未解决,如何取消上次承诺?



假设我有一个搜索函数来进行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')})
需要查看"搜索 1 已解决">

"搜索 2 被拒绝"搜索 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 调用函数被命名为httpRequest(promise令人困惑(:

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()方法。请注意,虽然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的插入顺序迭代来仅取消在返回刚刚结算的承诺的调用之前进行的调用返回的挂起承诺。


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

cancellation()所做的只是使返回的承诺的内部状态从挂起转换为拒绝,而不是后面的承诺首先解决,以便使用代码将调用用于承诺解析的适当处理程序。

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

最新更新