基本上场景是这样的:我在UI上使用了一个自动完成搜索功能,该请求被委托给后端进行搜索。虽然这个网络请求(#1)可能需要一点时间,但用户继续输入,最终触发了另一个后端调用(#2)。
在这里,#2自然优先于#1,因此我想取消Promise封装的#1请求。我已经在数据层中有了所有Promise的缓存,因此理论上,当我尝试提交#2的Promise时,可以检索到#1的Promise。
但是,我如何取消从缓存中检索出来的Promise #1呢?
有人能提供一种方法吗?
承诺已经解决(哈哈),似乎永远不可能取消一个(未决定的)承诺。
相反,有一个跨平台(Node,浏览器等)取消原语作为WHATWG的一部分(一个还构建HTML的标准机构),称为AbortController
。您可以将其用于取消返回承诺而不是承诺本身的函数:
// Take a signal parameter in the function that needs cancellation
async function somethingIWantToCancel({ signal } = {}) {
// either pass it directly to APIs that support it
// (fetch and most Node APIs do)
const response = await fetch('.../', { signal });
// return response.json;
// or if the API does not already support it -
// manually adapt your code to support signals:
const onAbort = (e) => {
// run any code relating to aborting here
};
signal.addEventListener('abort', onAbort, { once: true });
// and be sure to clean it up when the action you are performing
// is finished to avoid a leak
// ... sometime later ...
signal.removeEventListener('abort', onAbort);
}
// Usage
const ac = new AbortController();
setTimeout(() => ac.abort(), 1000); // give it a 1s timeout
try {
await somethingIWantToCancel({ signal: ac.signal });
} catch (e) {
if (e.name === 'AbortError') {
// deal with cancellation in caller, or ignore
} else {
throw e; // don't swallow errors :)
}
}
ES6的Promise目前还不支持取消操作。这个功能正在开发中,很多人都在为此努力工作。正确实现取消语义是很困难的,这需要时间来完善。在“fetch”仓库、esdiscuss和其他一些仓库上都有有趣的辩论,但如果我是你,我会耐心等待。
确实很重要,事实上,在客户端编程中,取消操作非常重要。像中止Web请求之类的场景非常普遍。
是的,对此我们深表歉意。在进一步规定之前,Promise必须先得到实现,因此它们被引入时缺少了一些有用的东西,比如finally和cancel方法。但这个问题正在通过DOM规范得到解决。取消操作并不是事后想到的,只是受时间限制和API设计更迭的影响。
你有几种选择:
使用第三方库显然很容易。至于令牌,你可以让你的方法接受一个函数并调用它,例如:
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;
});
};
这将使您能够:
var token = {};
var promise = getWithCancel("/someUrl", token);
// later we want to abort the promise:
token.cancel();
last
使用令牌方法并不难:
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);
};
}
这将让你可以:
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
});
不,像Bacon和Rx这样的库在这里并没有“闪光点”,因为它们只是可观察的库,它们与用户级别的promise库具有相同的优势,即不受规范限制。我猜我们将在ES2016中等待并看到observables成为本地支持。然而,在输入提示方面,它们确实很棒。
let controller = new AbortController();
let task = new Promise((resolve, reject) => {
const abortListener = ({target}) => {
controller.signal.removeEventListener('abort', abortListener);
reject(target.reason);
}
controller.signal.addEventListener('abort', abortListener);
// some logic ...
});
controller.abort('cancelled reason'); // task is now in rejected state
const res = task.catch((err) => (
controller.signal.aborted
? { value: err }
: null
));
xhr.onload = () => {
if(controller.signal.aborted) reject(controller.signal.reason);
resolve(xhr.responseText);
}
let controller = new AbortController();
fetch(url, {
signal: controller.signal
});
let controller = new AbortController();
fetch(url, controller);
controller.abort();
可取消的promise的标准提案已经失败。
Promise不是异步动作控制面板,会混淆所有者和消费者。相反,创建可以通过传入的令牌被取消的异步函数。
另一个promise成为了很好的令牌,使用Promise.race
使取消变得容易:
示例: 使用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">
在这里,我们通过注入一个undefined
的结果并测试它来“取消”以前的搜索,但我们可以轻松想象改为使用"CancelledError"
进行拒绝。
当然,这实际上并没有取消网络搜索,但这是fetch
的限制。如果fetch
接受一个取消的Promise作为参数,那么它就可以取消网络活动。
我在es-discuss中提出了这个“取消Promise模式”,正是为了建议fetch
这样做。
我查看了 Mozilla JS 参考文档并找到了这个链接:
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
我们来看一下:
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
});
我们这里有p1和p2作为Promise.race(...)
的参数,实际上这将创建一个新的resolve promise,这正是你所需的。
CancellationTokenSource
/CancellationToken
方法。在我的经验中,这些方法非常方便,在管理应用程序中实现健壮的取消逻辑。CancellationToken
和Deferred
)。// by @noseratio
// https://gist.github.com/noseratio/141a2df292b108ec4c147db4530379d2
// https://runkit.com/noseratio/cancellablepromise
const prex = require('prex');
/**
* A cancellable promise.
* @extends Promise
*/
class CancellablePromise extends Promise {
static get [Symbol.species]() {
// tinyurl.com/promise-constructor
return Promise;
}
constructor(executor, token) {
const withCancellation = async () => {
// create a new linked token source
const linkedSource = new prex.CancellationTokenSource(token? [token]: []);
try {
const linkedToken = linkedSource.token;
const deferred = new prex.Deferred();
linkedToken.register(() => deferred.reject(new prex.CancelError()));
executor({
resolve: value => deferred.resolve(value),
reject: error => deferred.reject(error),
token: linkedToken
});
await deferred.promise;
}
finally {
// this will also free all linkedToken registrations,
// so the executor doesn't have to worry about it
linkedSource.close();
}
};
super((resolve, reject) => withCancellation().then(resolve, reject));
}
}
/**
* A cancellable delay.
* @extends Promise
*/
class Delay extends CancellablePromise {
static get [Symbol.species]() { return Promise; }
constructor(delayMs, token) {
super(r => {
const id = setTimeout(r.resolve, delayMs);
r.token.register(() => clearTimeout(id));
}, token);
}
}
// main
async function main() {
const tokenSource = new prex.CancellationTokenSource();
const token = tokenSource.token;
setTimeout(() => tokenSource.cancel(), 2000); // cancel after 2000ms
let delay = 1000;
console.log(`delaying by ${delay}ms`);
await new Delay(delay, token);
console.log("successfully delayed."); // we should reach here
delay = 2000;
console.log(`delaying by ${delay}ms`);
await new Delay(delay, token);
console.log("successfully delayed."); // we should not reach here
}
main().catch(error => console.error(`Error caught, ${error}`));
await
或then
)的时候,取消操作也可能已经被触发了。如何处理这场竞赛取决于您,但多次调用token.throwIfCancellationRequested()
并不会有坏处,就像我上面所做的那样。最近我遇到了类似的问题。
我有一个基于Promise的客户端(不是网络客户端),我希望始终将最新请求的数据提供给用户,以保持UI的流畅性。
在苦苦挣扎了取消想法、Promise.race(...)
和Promise.all(..)
之后,我开始记住我的最后一个请求ID,并且当Promise实现时,只有在它匹配最后一个请求的ID时才呈现我的数据。
希望对某些人有所帮助。
您可以在完成之前让承诺拒绝:
// 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
不幸的是,fetch调用已经完成了,所以您将在网络选项卡中看到调用正在解决。 您的代码将忽略它。
我有一个异步函数,需要在用户输入时取消它,但它是一个涉及鼠标控制的长时间运行的函数。
我使用了p-queue,并将函数中的每一行添加到其中,并有一个可观察对象,我将取消信号提供给它。队列开始处理的任何内容都将无论如何运行,但您应该能够通过清除队列来取消之后的任何内容。您可以懒惰地将整个代码块抛入队列中,而不是像示例中的单行代码。
p-queue releases版本6与commonjs一起使用,7+切换到ESM可能会破坏您的应用程序。这破坏了我的electron/typescript/webpack应用。
const cancellable_function = async () => {
const queue = new PQueue({concurrency:1});
queue.pause();
queue.addAll([
async () => await move_mouse({...}),
async () => await mouse_click({...}),
])
for await (const item of items) {
queue.addAll([
async () => await do_something({...}),
async () => await do_something_else({...}),
])
}
const {information} = await get_information();
queue.addAll([
async () => await move_mouse({...}),
async () => await mouse_click({...}),
])
cancel_signal$.pipe(take(1)).subscribe(() => {
queue.clear();
});
queue.start();
await queue.onEmpty()
}
AbortController
我已经研究了几天,仍然觉得在中止事件处理程序内拒绝承诺只是方法的一部分。
问题在于,正如您所知,仅拒绝承诺会使代码等待它恢复执行,但如果有任何代码在拒绝或解决承诺之后运行,或者在其执行范围之外运行,例如在事件侦听器或异步调用内部,它将继续运行,浪费周期甚至可能浪费内存,这些都是不必要的。
执行下面的片段时,2秒后,控制台将包含来自承诺拒绝执行和挂起工作的任何输出。 承诺将被拒绝,等待它的工作可以继续,但工作将不会继续进行,这是我认为这个练习的主要点。
let abortController = new AbortController();
new Promise( ( resolve, reject ) => {
if ( abortController.signal.aborted ) return;
let abortHandler = () => {
reject( 'Aborted' );
};
abortController.signal.addEventListener( 'abort', abortHandler );
setTimeout( () => {
console.log( 'Work' );
console.log( 'More work' );
resolve( 'Work result' );
abortController.signal.removeEventListener( 'abort', abortHandler );
}, 2000 );
} )
.then( result => console.log( 'then:', result ) )
.catch( reason => console.error( 'catch:', reason ) );
setTimeout( () => abortController.abort(), 1000 );
if ( abortController.signal.aborted ) return;
在执行工作的代码中添加明智的代码点,以便工作不会被执行,并且如果必要,可以优雅地停止(在上面的if块中返回之前添加更多语句)。
这种方法让我想起了几年前的可取消令牌提案,但它实际上可以防止无用的工作。现在控制台输出应该只是终止错误,甚至当工作正在进行时,然后在中间被取消时,它也可以在处理的明智步骤中停止,例如在循环体的开头。
let abortController = new AbortController();
new Promise( ( resolve, reject ) => {
if ( abortController.signal.aborted ) return;
let abortHandler = () => {
reject( 'Aborted' );
};
abortController.signal.addEventListener( 'abort', abortHandler );
setTimeout( () => {
if ( abortController.signal.aborted ) return;
console.log( 'Work' );
if ( abortController.signal.aborted ) return;
console.log( 'More work' );
resolve( 'Work result' );
abortController.signal.removeEventListener( 'abort', abortHandler );
}, 2000 );
} )
.then( result => console.log( 'then:', result ) )
.catch( reason => console.error( 'catch:', reason ) );
setTimeout( () => abortController.abort(), 1000 );
.catch(_=>_)
附加到代码中以避免未来可能出现的未捕获Promise错误,这样做有什么问题吗?它会在虚空中悬浮吗? - Redu