Javascript 承诺 - 是否可以强制取消承诺
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/30233302/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Promise - is it possible to force cancel a promise
提问by Moonwalker
I use ES6 Promises to manage all of my network data retrieval and there are some situations where I need to force cancel them.
我使用 ES6 Promises 来管理我所有的网络数据检索,在某些情况下我需要强制取消它们。
Basically the scenario is such that I have a type-ahead search on the UI where the request is delegated to the backend has to carry out the search based on the partial input. While this network request (#1) may take a little bit of time, user continues to type which eventually triggers another backend call (#2)
基本上情况是这样的,我在 UI 上有一个预先输入的搜索,其中请求被委托给后端必须根据部分输入执行搜索。虽然这个网络请求 (#1) 可能需要一点时间,但用户继续输入最终会触发另一个后端调用 (#2)
Here #2 naturally takes precedence over #1 so I would like to cancel the Promise wrapping request #1. I already have a cache of all Promises in the data layer so I can theoretically retrieve it as I am attempting to submit a Promise for #2.
这里#2 自然优先于#1,所以我想取消Promise 包装请求#1。我已经在数据层中缓存了所有 Promise,因此理论上我可以在尝试为 #2 提交 Promise 时检索它。
But how do I cancel Promise #1 once I retrieve it from the cache?
但是,一旦我从缓存中检索它,我该如何取消 Promise #1?
Could anyone suggest an approach?
有人可以建议一种方法吗?
回答by Benjamin Gruenbaum
No. We can't do that yet.
不,我们还不能那样做。
ES6 promises do not support cancellation yet. It's on its way, and its design is something a lot of people worked really hard on. Soundcancellation semantics are hard to get right and this is work in progress. There are interesting debates on the "fetch" repo, on esdiscuss and on several other repos on GH but I'd just be patient if I were you.
ES6承诺不支持取消还没有。它正在进行中,它的设计是很多人非常努力的工作。声音消除语义很难做到正确,这是正在进行中的工作。关于“获取”存储库、esdiscuss 和其他几个关于 GH 的存储库存在有趣的争论,但如果我是你,我会耐心等待。
But, but, but.. cancellation is really important!
但是,但是,但是……取消真的很重要!
It is, the reality of the matter is cancellation is reallyan important scenario in client-side programming. The cases you describe like aborting web requests are important and they're everywhere.
事实是,取消确实是客户端编程中的一个重要场景。您描述的诸如中止 Web 请求之类的情况很重要,而且无处不在。
So... the language screwed me!
所以……语言把我搞砸了!
Yeah, sorry about that. Promises had to get in first before further things were specified - so they went in without some useful stuff like .finallyand .cancel- it's on its way though, to the spec through the DOM. Cancellation is notan afterthought it's just a time constraint and a more iterative approach to API design.
是的,对此感到抱歉。承诺必须得到在第一前进一步东西都规定-所以他们就去了,没有像一些有用的东西.finally,并.cancel-这是在它的途中,虽然,通过DOM规范。取消不是事后的想法,它只是时间限制和 API 设计的一种更迭代的方法。
So what can I do?
那我能做什么?
You have several alternatives:
您有几种选择:
- Use a third party library like bluebirdwho can move a lot faster than the spec and thus have cancellation as well as a bunch of other goodies - this is what large companies like WhatsApp do.
- Pass a cancellation token.
- 使用像bluebird这样的第三方库,它的移动速度比规范快得多,因此可以取消以及其他一些好东西——这就是像 WhatsApp 这样的大公司所做的。
- 传递一个取消令牌。
Using a third party library is pretty obvious. As for a token, you can make your method take a function in and then call it, as such:
使用第三方库是很明显的。至于令牌,您可以让您的方法接受一个函数然后调用它,如下所示:
function getWithCancel(url, token) { // the token is for cancellation
var xhr = new XMLHttpRequest;
xhr.open("GET", url);
return new Promise(function(resolve, reject) {
xhr.onload = function() { resolve(xhr.responseText); });
token.cancel = function() { // SPECIFY CANCELLATION
xhr.abort(); // abort request
reject(new Error("Cancelled")); // reject the promise
};
xhr.onerror = reject;
});
};
Which would let you do:
这会让你做:
var token = {};
var promise = getWithCancel("/someUrl", token);
// later we want to abort the promise:
token.cancel();
Your actual use case - last
您的实际用例 - last
This isn't too hard with the token approach:
使用令牌方法这并不太难:
function last(fn) {
var lastToken = { cancel: function(){} }; // start with no op
return function() {
lastToken.cancel();
var args = Array.prototype.slice.call(arguments);
args.push(lastToken);
return fn.apply(this, args);
};
}
Which would let you do:
这会让你做:
var synced = last(getWithCancel);
synced("/url1?q=a"); // this will get canceled
synced("/url1?q=ab"); // this will get canceled too
synced("/url1?q=abc"); // this will get canceled too
synced("/url1?q=abcd").then(function() {
// only this will run
});
And no, libraries like Bacon and Rx don't "shine" here because they're observable libraries, they just have the same advantage user level promise libraries have by not being spec bound. I guess we'll wait to have and see in ES2016 when observables go native. They arenifty for typeahead though.
不,像 Bacon 和 Rx 这样的库在这里不会“发光”,因为它们是可观察的库,它们只是具有与用户级承诺库相同的优势,不受规范约束。我想我们会等待并在 ES2016 中看到 observables 成为原生的时候。不过,它们非常适合提前输入。
回答by jib
Standard proposals for cancellable promises have failed.
可取消承诺的标准提案失败了。
A promise is not a control surface for the async action fulfilling it; confuses owner with consumer. Instead, create asynchronous functionsthat can be cancelled through some passed-in token.
promise 不是实现它的异步操作的控制面;混淆所有者和消费者。相反,创建可以通过一些传入令牌取消的异步函数。
Another promise makes a fine token, making cancel easy to implement with Promise.race:
另一个承诺是一个很好的令牌,使取消易于实现Promise.race:
Example:Use Promise.raceto cancel the effect of a previous chain:
例如:使用Promise.race取消先前链的影响:
let cancel = () => {};
input.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancel();
let p = new Promise(resolve => cancel = resolve);
Promise.race([p, getSearchResults(term)]).then(results => {
if (results) {
console.log(`results for "${term}"`,results);
}
});
}
function getSearchResults(term) {
return new Promise(resolve => {
let timeout = 100 + Math.floor(Math.random() * 1900);
setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
});
}
Search: <input id="input">
Here we're "cancelling" previous searches by injecting an undefinedresult and testing for it, but we could easily imagine rejecting with "CancelledError"instead.
在这里,我们通过注入一个undefined结果并对其进行测试来“取消”之前的搜索,但我们可以很容易地想象用"CancelledError"代替来拒绝。
Of course this doesn't actually cancel the network search, but that's a limitation of fetch. If fetchwere to take a cancel promise as argument, then it could cancel the network activity.
当然,这实际上并没有取消网络搜索,但这是fetch. 如果fetch将取消承诺作为参数,那么它可以取消网络活动。
I've proposedthis "Cancel promise pattern" on es-discuss, exactly to suggest that fetchdo this.
我在 es-discuss 上提出了这个“取消承诺模式”,正是为了建议这样fetch做。
回答by nikola-miljkovic
I have checked out Mozilla JS reference and found this:
我检查了 Mozilla JS 参考并发现了这个:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
Let's check it out:
让我们来看看:
var p1 = new Promise(function(resolve, reject) {
setTimeout(resolve, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log(value); // "two"
// Both resolve, but p2 is faster
});
We have here p1, and p2 put in Promise.race(...)as arguments, this is actually creating new resolve promise, which is what you require.
我们在这里将 p1 和 p2Promise.race(...)作为参数放入,这实际上是在创建新的解析承诺,这正是您所需要的。
回答by noseratio
For Node.js and Electron, I'd highly recommend using Promise Extensions for JavaScript (Prex). Its author Ron Bucktonis one of the key TypeScript engineers and also is the guy behind the current TC39's ECMAScript Cancellationproposal. The library is well documented and chances are some of Prex will make to the standard.
对于 Node.js 和 Electron,我强烈推荐使用Promise Extensions for JavaScript (Prex)。它的作者Ron Buckton是主要的 TypeScript 工程师之一,也是当前 TC39 的ECMAScript Cancellation提案的幕后推手。该库有很好的文档记录,并且 Prex 有可能符合标准。
On a personal note and coming from a heavy C# background, I like very much the fact that Prex is modelled upon the existing Cancellation in Managed Threadsframework, i.e. based on the approach taken with CancellationTokenSource/CancellationToken.NET APIs. In my experience, those have been very handy to implement robust cancellation logic in managed apps.
就个人而言,并且来自大量的 C# 背景,我非常喜欢 Prex以托管线程框架中现有的取消为模型的事实,即基于CancellationTokenSource/ CancellationToken.NET API所采用的方法。根据我的经验,这些对于在托管应用程序中实现强大的取消逻辑非常方便。
I also verified it to work within a browser by bundling Prex using Browserify.
我还通过使用Browserify捆绑 Prex 来验证它可以在浏览器中工作。
Here is an example of a delay with cancellation, using prex.CancellationTokenSource:
这是取消延迟的示例,使用prex.CancellationTokenSource:
// https://stackoverflow.com/a/53093799
const prex = require('prex');
// delayWithCancellation
function delayWithCancellation(timeoutMs, token) {
console.log(`delayWithCancellation: ${timeoutMs}`);
return createCancellablePromise((resolve, reject, setCancelListener) => {
token.throwIfCancellationRequested();
const id = setTimeout(resolve, timeoutMs);
setCancelListener(e => clearTimeout(id));
}, token);
}
// main
async function main() {
const tokenSource = new prex.CancellationTokenSource();
setTimeout(() => tokenSource.cancel(), 2000); // cancel after 1500ms
const token = tokenSource.token;
await delayWithCancellation(1000, token);
console.log("successfully delayed."); // we should reach here
await delayWithCancellation(1500, token);
console.log("successfully delayed."); // we should not reach here
}
// createCancellablePromise - create a Promise that gets rejected
// when cancellation is requested on the token source
async function createCancellablePromise(executor, token) {
if (!token) {
return await new Promise(executor);
}
// prex.Deferred is similar to TaskCompletionsource in .NET
const d = new prex.Deferred();
// function executor(resolve, reject, setCancelListener)
// function oncancel(CancelError)
executor(d.resolve, d.reject, oncancel =>
d.oncancel = oncancel);
const reg = token.register(() => {
// the token cancellation callback is synchronous,
// and so is the d.oncancel callback
try {
// capture the CancelError
token.throwIfCancellationRequested();
}
catch (e) {
try {
d.oncancel && d.oncancel(e);
// reject here if d.oncancel did not resolve/reject
d.reject(e);
}
catch (e2) {
d.reject(e2);
}
}
});
try {
await d.promise;
}
finally {
reg.unregister();
}
}
main().catch(e => console.log(e));
Note that cancellation is a race. I.e., a promise may have been resolved successfully, but by the time you observe it (with awaitor then), the cancellation may have been triggered as well. It's up to you how you handle this race, but it doesn't hurts to call token.throwIfCancellationRequested()an extra time, like I do above.
请注意,取消是一场竞赛。即,一个承诺可能已成功解决,但当您观察它时(使用await或then),取消也可能已被触发。这取决于你如何处理这场比赛,但token.throwIfCancellationRequested()像我上面所做的那样,打电话给额外的时间并没有什么坏处。
回答by Igor S?omski
I faced similar problem recently.
我最近遇到了类似的问题。
I had a promise based client (not a network one) and i wanted to always give the latest requested data to the user to keep the UI smooth.
我有一个基于 promise 的客户端(不是网络客户端),我希望始终向用户提供最新请求的数据以保持 UI 流畅。
After struggling with cancellation idea, Promise.race(...)and Promise.all(..)i just started remembering my last request id and when promise was fulfilled i was only rendering my data when it matched the id of a last request.
与消除思想挣扎后,Promise.race(...)和Promise.all(..)我刚开始记住我最后的请求ID和承诺应验了,那时我才呈现我的数据时,它相匹配的最后一个请求的ID。
Hope it helps someone.
希望它可以帮助某人。
回答by Devi
See https://www.npmjs.com/package/promise-abortable
见https://www.npmjs.com/package/promise-abortable
$ npm install promise-abortable
回答by Rashomon
You can make the promise reject before finishing:
您可以在完成之前拒绝承诺:
// Our function to cancel promises receives a promise and return the same one and a cancel function
const cancellablePromise = (promiseToCancel) => {
let cancel
const promise = new Promise((resolve, reject) => {
cancel = reject
promiseToCancel
.then(resolve)
.catch(reject)
})
return {promise, cancel}
}
// A simple promise to exeute a function with a delay
const waitAndExecute = (time, functionToExecute) => new Promise((resolve, reject) => {
timeInMs = time * 1000
setTimeout(()=>{
console.log(`Waited ${time} secs`)
resolve(functionToExecute())
}, timeInMs)
})
// The promise that we will cancel
const fetchURL = () => fetch('https://pokeapi.co/api/v2/pokemon/ditto/')
// Create a function that resolve in 1 seconds. (We will cancel it in 0.5 secs)
const {promise, cancel} = cancellablePromise(waitAndExecute(1, fetchURL))
promise
.then((res) => {
console.log('then', res) // This will executed in 1 second
})
.catch(() => {
console.log('catch') // We will force the promise reject in 0.5 seconds
})
waitAndExecute(0.5, cancel) // Cancel previous promise in 0.5 seconds, so it will be rejected before finishing. Commenting this line will make the promise resolve
Unfortunately the fetch call has already be done, so you will see the call resolving in the Network tab. Your code will just ignore it.
不幸的是,提取调用已经完成,因此您将在“网络”选项卡中看到正在解析的调用。您的代码将忽略它。
回答by allenyllee
Because @jib reject my modify, so I post my answer here. It's just the modfify of @jib's anwserwith some comments and using more understandable variable names.
因为@jib 拒绝我的修改,所以我在这里发布我的答案。这只是@jib的 anwser 的修改,带有一些注释并使用更易于理解的变量名称。
Below I just show examples of two different method: one is resolve() the other is reject()
下面我只展示了两种不同方法的示例:一种是 resolve() 另一种是 reject()
let cancelCallback = () => {};
input.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancelCallback(); //cancel previous promise by calling cancelCallback()
let setCancelCallbackPromise = () => {
return new Promise((resolve, reject) => {
// set cancelCallback when running this promise
cancelCallback = () => {
// pass cancel messages by resolve()
return resolve('Canceled');
};
})
}
Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
// check if the calling of resolve() is from cancelCallback() or getSearchResults()
if (results == 'Canceled') {
console.log("error(by resolve): ", results);
} else {
console.log(`results for "${term}"`, results);
}
});
}
input2.oninput = function(ev) {
let term = ev.target.value;
console.log(`searching for "${term}"`);
cancelCallback(); //cancel previous promise by calling cancelCallback()
let setCancelCallbackPromise = () => {
return new Promise((resolve, reject) => {
// set cancelCallback when running this promise
cancelCallback = () => {
// pass cancel messages by reject()
return reject('Canceled');
};
})
}
Promise.race([setCancelCallbackPromise(), getSearchResults(term)]).then(results => {
// check if the calling of resolve() is from cancelCallback() or getSearchResults()
if (results !== 'Canceled') {
console.log(`results for "${term}"`, results);
}
}).catch(error => {
console.log("error(by reject): ", error);
})
}
function getSearchResults(term) {
return new Promise(resolve => {
let timeout = 100 + Math.floor(Math.random() * 1900);
setTimeout(() => resolve([term.toLowerCase(), term.toUpperCase()]), timeout);
});
}
Search(use resolve): <input id="input">
<br> Search2(use reject and catch error): <input id="input2">

