承诺-是否有可能强迫取消一个承诺

我使用 ES6承诺来管理我所有的网络数据检索和有些情况下,我需要强制取消他们。

基本上这个场景是这样的,我在 UI 上有一个预输入搜索,其中请求被委托给后端,必须根据部分输入执行搜索。虽然这个网络请求(# 1)可能需要一点时间,但用户继续输入,最终会触发另一个后端调用(# 2)

在这里,# 2自然优先于 # 1,因此我想取消誓言包装请求 # 1。我已经有一个缓存的所有承诺在数据层,所以我可以从理论上检索它,因为我试图提交一个承诺 # 2。

但是一旦我从缓存中取回了一号承诺,我该如何取消它呢?

有人能提出一个方案吗?

105085 次浏览

我查看了 Mozilla JS 的参考资料,发现:

Https://developer.mozilla.org/en-us/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(...)中,这实际上创建了新的解析承诺,这正是您所需要的。

在现代 JavaScript 中-不

承诺已经定下来了(哈) ,看起来永远不可能取消一个(待定的)承诺。

相反,作为 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承诺不支持取消 还没有。它已经在路上了,它的设计是很多人非常努力的工作。声音取消语义很难正确理解,这是正在进行的工作。有有趣的辩论在“取回”回购,在埃斯讨论和其他几个回购的生长激素,但我只是耐心如果我是你。

但是,但是,但是,取消是非常重要的!

是的,现实中的问题是取消 真的是客户端编程中的一个重要场景。你所描述的像中止网络请求这样的情况非常重要,而且无处不在。

所以... 语言把我搞糊涂了!

是啊,抱歉。承诺必须在进一步的事情被指定之前首先进入-所以他们进入没有一些有用的东西,如 .finally.cancel-它是在它的道路上,通过 DOM 的规范。取消是 没有的一个事后想法,它只是一个时间限制和一个更迭代的 API 设计方法。

那我能做什么?

你有几个选择:

  • 使用像 青鸟这样的第三方库,它可以比规范快很多,因此可以取消,也可以取消其他一些好东西——这就是像 WhatsApp 这样的大公司所做的。
  • 通过取消 Token

使用第三方库是显而易见的。对于标记,您可以让您的方法接受一个函数,然后调用它,如下所示:

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 这样的库在这里不会“发光”,因为它们是可观察到的库,它们只是和用户级别的承诺库一样,具有不受规范限制的优势。我想我们将等待看到 ES2016当可观测的东西变得本地化。他们 的打字机虽然漂亮。

关于取消承诺的标准提案已经失败。

允诺不是实现它的异步操作的控制面; 它将所有者与使用者混淆了。相反,创建可以通过某种传入令牌取消的异步 功能

另一个承诺是一个很好的标志,使取消很容易与 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">

Here we're "cancelling" previous searches by injecting an undefined result and testing for it, but we could easily imagine rejecting with "CancelledError" instead.

Of course this doesn't actually cancel the network search, but that's a limitation of fetch. If fetch were to take a cancel promise as argument, then it could cancel the network activity.

I've proposed this "Cancel promise pattern" on es-discuss, exactly to suggest that fetch do this.

我最近也遇到过类似的问题。

我有一个承诺为基础的客户端(而不是网络之一) ,我想总是给用户最新的请求数据,以保持用户界面顺利。

在与取消的想法斗争后,Promise.race(...)Promise.all(..)我只是开始记住我的最后一个请求标识,当承诺实现时,我只是呈现我的数据时,它匹配的标识最后一个请求。

希望能帮到别人。

对于 Node.js 和 Electron,我强烈推荐使用 JavaScript 承诺扩展(Prex)。它的作者 Ron Buckton是关键的 TypeScript 工程师之一,也是当前 TC39的 ECMAScript 取消提案的幕后推手。这个库有很好的文档记录,Prex 的一些内容可能会达到标准。

就个人而言,作为 C # 背景,我非常喜欢 Prex 是以现有的 托管线程中的取消框架为模型的事实,即基于 CancellationTokenSource/CancellationToken所采用的方法。NET 应用程式介面。根据我的经验,这对于在托管应用程序中实现健壮的取消逻辑非常方便。

我还通过使用 浏览器化捆绑 Prex 来验证它在浏览器中的工作性。

下面是一个取消延迟的例子(要点RunKit,使用 Prex作为它的 CancellationTokenDeferred) :

// 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()加时也不会有什么坏处。

参见 https://www.npmjs.com/package/promise-abortable

$ npm install promise-abortable

因为@jib 拒绝了我的修改,所以我在这里发布了我的答案。这只是对 @ jib 的回答的修改,添加了一些注释,并使用了更易于理解的变量名。

下面我将展示两种不同方法的示例: 一种是  決() ,另一种是拒絕()

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">

你可以在完成之前拒绝承诺:

// 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

不幸的是,提取调用已经完成,因此您将在 Network 选项卡中看到调用解析。你的代码会忽略它。

使用外部程序包提供的  种子类,这可以按照以下步骤完成: 现场演示

import CPromise from "c-promise2";


function fetchWithTimeout(url, {timeout, ...fetchOptions}= {}) {
return new CPromise((resolve, reject, {signal}) => {
fetch(url, {...fetchOptions, signal}).then(resolve, reject)
}, timeout)
}


const chain= fetchWithTimeout('http://localhost/')
.then(response => response.json())
.then(console.log, console.warn);


//chain.cancel(); call this to abort the promise and releated request

与中止控制器一起

可以使用中止控制器拒绝承诺或根据您的要求解决:

let controller = new AbortController();


let task = new Promise((resolve, reject) => {
// some logic ...
controller.signal.addEventListener('abort', () => {
reject('oops'));
}
});


controller.abort(); // task is now in rejected state

此外,最好在中止时删除事件侦听器,以防止内存泄漏

如果您想检查任务是否中止并返回,那么这个承诺将永远处于 pending状态。但是在这种情况下,如果你的意图是这样的话,你也不会因为任何错误而解雇 .catch:

controller.abort();
new Promise((resolve, reject) => {
if(controller.signal.aborted) return;
}

相同的工作取消获取:

let controller = new AbortController();
fetch(url, {
signal: controller.signal
});

或者直接通过控制器:

let controller = new AbortController();
fetch(url, controller);

并调用 abort 方法取消传递此控制器的一个或无限次读取 controller.abort();

使用 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 );

我发现这里发布的解决方案有点难以阅读,所以我创建了一个我认为更容易使用的 helper 函数。

无论当前调用是否已经过时,helper 函数都提供对信息的访问。有了这些信息,函数本身就必须相应地处理事情(通常只是简单地返回)。

// Typescript
export function obsoletableFn<Res, Args extends unknown[]>(
fn: (isObsolete: () => boolean, ...args: Args) => Promise<Res>,
): (...args: Args) => Promise<Res> {
let lastCaller = null;


return (...args: Args) => {
const me = Symbol();
lastCaller = me;


const isObsolete = () => lastCaller !== me;


return fn(isObsolete, ...args);
};
}

// helper function
function obsoletableFn(fn) {
let lastCaller = null;
return (...args) => {
const me = Symbol();
lastCaller = me;
const isObsolete = () => lastCaller !== me;
return fn(isObsolete, ...args);
};
}


const simulateRequest = () => new Promise(resolve => setTimeout(resolve, Math.random() * 2000 + 1000));


// usage
const myFireAndForgetFn = obsoletableFn(async(isObsolete, x) => {
console.log(x, 'starting');
await simulateRequest();
if (isObsolete()) {
console.log(x, 'is obsolete');
// return, as there is already a more recent call running
return;
}
console.log(x, 'is not obsolete');
document.querySelector('div').innerHTML = `Response ${x}`;
});


myFireAndForgetFn('A');
myFireAndForgetFn('B');
<div>Waiting for response...</div>

所以我有一个异步函数,我需要取消用户输入,但它是一个长期运行的一个涉及鼠标控制。

我使用 P-queue,并添加每一行在我的函数到它,并有一个可观察的,我饲料的取消信号。队列开始处理的任何事情都将运行,但是您应该能够通过清除队列来取消之后的任何事情。添加到队列中的任务越短,获得取消信号后退出的时间就越早。您可以懒惰地将整个代码块抛入队列中,而不是我在示例中使用的一行代码。

P-queue release Version 6使用 commondjs,7 + 切换到 ESM,可能会破坏你的应用程序。

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()
}