取消一个普通的 ECMAScript 6承诺链

是否有清除 JavaScriptPromise实例的 .then的方法?

我在 QUnit之上编写了一个 JavaScript 测试框架。框架通过在 Promise中运行每个测试来同步运行测试。(对不起,这个代码块的长度。我尽力评论了一下,这样就不会觉得乏味了。)

/* Promise extension -- used for easily making an async step with a
timeout without the Promise knowing anything about the function
it's waiting on */
$$.extend(Promise, {
asyncTimeout: function (timeToLive, errorMessage) {
var error = new Error(errorMessage || "Operation timed out.");
var res, // resolve()
rej, // reject()
t,   // timeout instance
rst, // reset timeout function
p,   // the promise instance
at;  // the returned asyncTimeout instance


function createTimeout(reject, tempTtl) {
return setTimeout(function () {
// triggers a timeout event on the asyncTimeout object so that,
// if we want, we can do stuff outside of a .catch() block
// (may not be needed?)
$$(at).trigger("timeout");


reject(error);
}, tempTtl || timeToLive);
}


p = new Promise(function (resolve, reject) {
if (timeToLive != -1) {
t = createTimeout(reject);


// reset function -- allows a one-time timeout different
//    from the one original specified
rst = function (tempTtl) {
clearTimeout(t);
t = createTimeout(reject, tempTtl);
}
} else {
// timeToLive = -1 -- allow this promise to run indefinitely
// used while debugging
t = 0;
rst = function () { return; };
}


res = function () {
clearTimeout(t);
resolve();
};


rej = reject;
});


return at = {
promise: p,
resolve: res,
reject: rej,
reset: rst,
timeout: t
};
}
});


/* framework module members... */


test: function (name, fn, options) {
var mod = this; // local reference to framework module since promises
// run code under the window object


var defaultOptions = {
// default max running time is 5 seconds
timeout: 5000
}


options = $$.extend({}, defaultOptions, options);


// remove timeout when debugging is enabled
options.timeout = mod.debugging ? -1 : options.timeout;


// call to QUnit.test()
test(name, function (assert) {
// tell QUnit this is an async test so it doesn't run other tests
// until done() is called
var done = assert.async();
return new Promise(function (resolve, reject) {
console.log("Beginning: " + name);


var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
$$(at).one("timeout", function () {
// assert.fail() is just an extension I made that literally calls
// assert.ok(false, msg);
assert.fail("Test timed out");
});


// run test function
var result = fn.call(mod, assert, at.reset);


// if the test returns a Promise, resolve it before resolving the test promise
if (result && result.constructor === Promise) {
// catch unhandled errors thrown by the test so future tests will run
result.catch(function (error) {
var msg = "Unhandled error occurred."
if (error) {
msg = error.message + "\n" + error.stack;
}


assert.fail(msg);
}).then(function () {
// resolve the timeout Promise
at.resolve();
resolve();
});
} else {
// if test does not return a Promise, simply clear the timeout
// and resolve our test Promise
at.resolve();
resolve();
}
}).then(function () {
// tell QUnit that the test is over so that it can clean up and start the next test
done();
console.log("Ending: " + name);
});
});
}

如果测试超时,我的超时承诺将在测试上标记为 assert.fail(),以便测试被标记为失败,这很好,但是测试将继续运行,因为测试承诺(result)仍然在等待解决它。

我需要一个取消考试的好办法。我可以通过在框架模块 this.cancelTest上创建一个字段或者其他东西来做到这一点,并且在测试中经常检查(例如在每次 then()迭代的开始)是否要取消。但是,理想情况下,我可以使用 $$(at).on("timeout", /* something here */)来清除 result变量上剩余的 then(),这样就不会运行测试的其余部分。

这种东西真的存在吗?

快速更新

我试过用 Promise.race([result, at.promise])没用。

更新2 + 混乱

为了解除阻塞,我在测试思想中添加了几行带有 mod.cancelTest/轮询的代码。(我还删除了事件触发器。)

return new Promise(function (resolve, reject) {
console.log("Beginning: " + name);


var at = Promise.asyncTimeout(options.timeout, "Test timed out.");
at.promise.catch(function () {
// end the test if it times out
mod.cancelTest = true;
assert.fail("Test timed out");
resolve();
});


// ...
    

}).then(function () {
// tell QUnit that the test is over so that it can clean up and start the next test
done();
console.log("Ending: " + name);
});

我在 catch语句中设置了一个断点,它被命中了。现在让我困惑的是 then()语句没有被调用。有什么想法吗?

更新3

搞清楚了最后一件事。fn.call()抛出了一个我没有发现的错误,所以在 at.promise.catch()解决它之前,测试承诺被拒绝了。

157033 次浏览

有没有一种方法可以清除 JavaScript 承诺实例的 .then

没有。至少 ECMAScript 6中没有。默认情况下,承诺(及其 then处理程序)是不可取消的 (很不幸)。关于如何以正确的方式实现这一点,电子讨论(例如 给你)中有一些讨论,但是无论哪种方法都不会赢得 ES6的支持。

目前的观点是,子类化允许使用自己的实现 (不知道这样行不行)创建可取消的承诺。

在语言委员会找到 (希望是 ES7?)的最佳方式之前,你仍然可以使用用户界面的承诺实现,其中许多功能取消了。

目前的讨论在 https://github.com/domenic/cancelable-promisehttps://github.com/bergus/promise-cancellation草案中。

虽然在 ES6中没有这样做的标准方法,但是有一个名为 青鸟的库来处理这个问题。

作为反应文档的一部分,还有一种推荐的方法。它看起来与您在第2次和第3次更新中的内容类似。

const makeCancelable = (promise) => {
let hasCanceled_ = false;


const wrappedPromise = new Promise((resolve, reject) => {
promise.then((val) =>
hasCanceled_ ? reject({isCanceled: true}) : resolve(val)
);
promise.catch((error) =>
hasCanceled_ ? reject({isCanceled: true}) : reject(error)
);
});


return {
promise: wrappedPromise,
cancel() {
hasCanceled_ = true;
},
};
};


const cancelablePromise = makeCancelable(
new Promise(r => component.setState({...}}))
);


cancelablePromise
.promise
.then(() => console.log('resolved'))
.catch((reason) => console.log('isCanceled', reason.isCanceled));


cancelablePromise.cancel(); // Cancel the promise

摘自: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html

如果你想阻止所有的 then/catch 被执行,你可以通过注入一个永远不会解决的承诺来做到这一点。它可能有内存泄漏的后处理,但是它将修复这个问题,并且不应该在大多数应用程序中造成太多的内存浪费。

new Promise((resolve, reject) => {
console.log('first chain link executed')
resolve('daniel');
}).then(name => {
console.log('second chain link executed')
if (name === 'daniel') {
// I don't want to continue the chain, return a new promise
// that never calls its resolve function
return new Promise((resolve, reject) => {
console.log('unresolved promise executed')
});
}
}).then(() => console.log('last chain link executed'))


// VM492:2 first chain link executed
// VM492:5 second chain link executed
// VM492:8 unresolved promise executed

简单版 :

只要发出拒绝函数。

简单的想法:

function MyPromise(myparams,cancel_holder) {
return new Promise(function(resolve,reject){
//do work here
cancel_holder.cancel=reject
}
}

或简单的想法2:

function MyPromise() {
var cancel_holder={};
var promise=new Promise(function(resolve,reject){
//do work here
cancel_holder.cancel=reject;
}
promise.cancel=function(){ cancel_holder.cancel(); }
return promise;
}

例如:

function Sleep(ms,cancel_holder) {


return new Promise(function(resolve,reject){
var done=false;
var t=setTimeout(function(){if(done)return;done=true;resolve();}, ms);
cancel_holder.cancel=function(){if(done)return;done=true;if(t)clearTimeout(t);reject();}
})
}

包装溶液(工厂)

我找到的解决方案是传递一个 Cancelholder 对象。它将有一个取消函数。如果它有一个取消函数,那么它是可取消的。

此取消函数拒绝带有 Error (“已取消”)的承诺。

在解析、拒绝或 on _ Cancel 之前,防止无理由地调用取消函数。

我发现方便通过注射取消行动

function cancelablePromise(cancel_holder,promise_fn,optional_external_cancel) {
if(!cancel_holder)cancel_holder={};
return new Promise( function(resolve,reject) {
var canceled=false;
var resolve2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; resolve.apply(this,arguments);}
var reject2=function(){ if(canceled) return; canceled=true; delete cancel_holder.cancel; reject.apply(this,arguments);}
var on_cancel={}
cancel_holder.cancel=function(){
if(canceled) return; canceled=true;


delete cancel_holder.cancel;
cancel_holder.canceled=true;


if(on_cancel.cancel)on_cancel.cancel();
if(optional_external_cancel)optional_external_cancel();


reject(new Error('canceled'));
};
    

return promise_fn.call(this,resolve2,reject2,on_cancel);
});
}
 

function Sleep(ms,cancel_holder) {


return cancelablePromise(cancel_holder,function(resolve,reject,oncacnel){


var t=setTimeout(resolve, ms);
oncacnel.cancel=function(){if(t)clearTimeout(t);}


})
}




let cancel_holder={};


// meanwhile in another place it can be canceled
setTimeout(function(){  if(cancel_holder.cancel)cancel_holder.cancel(); },500)


Sleep(1000,cancel_holder).then(function() {
console.log('sleept well');
}, function(e) {
if(e.message!=='canceled') throw e;
console.log('sleep interrupted')
})

这是我们的实现 https://github.com/permettez-moi-de-construire/cancellable-promise

就像

const {
cancellablePromise,
CancelToken,
CancelError
} = require('@permettezmoideconstruire/cancellable-promise')


const cancelToken = new CancelToken()


const initialPromise = SOMETHING_ASYNC()
const wrappedPromise = cancellablePromise(initialPromise, cancelToken)




// Somewhere, cancel the promise...
cancelToken.cancel()




//Then catch it
wrappedPromise
.then((res) => {
//Actual, usual fulfill
})
.catch((err) => {
if(err instanceOf CancelError) {
//Handle cancel error
}


//Handle actual, usual error
})

其中:

  • 不接触承诺 API
  • 让我们进一步取消 catch电话
  • 依靠取消是 被拒绝了而不是 解决了不像任何其他建议或实施

欢迎各界人士提出意见和建议

设置一个“取消”的承诺属性,以信号 then()catch()退出早。它非常有效,特别是在 Web Workers 中,现有的微任务在 onmessage处理程序的誓言中排队。

// Queue task to resolve Promise after the end of this script
const promise = new Promise(resolve => setTimeout(resolve))


promise.then(_ => {
if (promise.canceled) {
log('Promise cancelled.  Exiting early...');
return;
}


log('No cancelation signaled.  Continue...');
})


promise.canceled = true;


function log(msg) {
document.body.innerHTML = msg;
}

我真的很惊讶,竟然没有人提到 Promise.race作为这方面的候选者:

const actualPromise = new Promise((resolve, reject) => { setTimeout(resolve, 10000) });
let cancel;
const cancelPromise = new Promise((resolve, reject) => {
cancel = reject.bind(null, { canceled: true })
})


const cancelablePromise = Object.assign(Promise.race([actualPromise, cancelPromise]), { cancel });

@ Michael Yagudaev 的回答对我有用。

但是最初的答案并没有把包装好的承诺和。Catch ()处理拒绝处理,以下是我对@Michael Yagudaev 的回答的改进:

const makeCancelablePromise = promise => {
let hasCanceled = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise
.then(val => (hasCanceled ? reject({ isCanceled: true }) : resolve(val)))
.catch(
error => (hasCanceled ? reject({ isCanceled: true }) : reject(error))
);
});


return {
promise: wrappedPromise,
cancel() {
hasCanceled = true;
}
};
};


// Example Usage:
const cancelablePromise = makeCancelable(
new Promise((rs, rj) => {
/*do something*/
})
);
cancelablePromise.promise.then(() => console.log('resolved')).catch(err => {
if (err.isCanceled) {
console.log('Wrapped promise canceled');
return;
}
console.log('Promise was not canceled but rejected due to errors: ', err);
});
cancelablePromise.cancel();

如果 p 是一个包含一个  诺言的变量,那么 p.then(empty);应该在它最终完成或者已经完成时取消该承诺(是的,我知道这不是最初的问题,但这是我的问题)。“空”是 function empty() {}。我只是个初学者,可能是错的,但是这些其他的答案似乎太复杂了。承诺应该很简单。

const makeCancelable = promise => {
let rejectFn;


const wrappedPromise = new Promise((resolve, reject) => {
rejectFn = reject;


Promise.resolve(promise)
.then(resolve)
.catch(reject);
});


wrappedPromise.cancel = () => {
rejectFn({ canceled: true });
};


return wrappedPromise;
};

用法:

const cancelablePromise = makeCancelable(myPromise);
// ...
cancelablePromise.cancel();

试试 承诺-流产: https://www.npmjs.com/package/promise-abortable

$ npm install promise-abortable
import AbortablePromise from "promise-abortable";


const timeout = new AbortablePromise((resolve, reject, signal) => {
setTimeout(reject, timeToLive, error);
signal.onabort = resolve;
});


Promise.resolve(fn()).then(() => {
timeout.abort();
});

实际上,要阻止承诺的履行是不可能的,但是你可以劫持被拒绝者,从承诺本身中调用它。

class CancelablePromise {
constructor(executor) {
let _reject = null;
const cancelablePromise = new Promise((resolve, reject) => {
_reject = reject;
return executor(resolve, reject);
});
cancelablePromise.cancel = _reject;


return cancelablePromise;
}
}

用法:

const p = new CancelablePromise((resolve, reject) => {
setTimeout(() => {
console.log('resolved!');
resolve();
}, 2000);
})


p.catch(console.log);


setTimeout(() => {
p.cancel(new Error('Messed up!'));
}, 1000);

AbortController的帮助下,承诺可以被取消。

有一个方法来清除然后: 是的,你可以拒绝承诺与 AbortController对象,然后 promise将绕过所有然后块,直接去捕捉块。

例如:

import "abortcontroller-polyfill";


let controller = new window.AbortController();
let signal = controller.signal;
let elem = document.querySelector("#status")


let example = (signal) => {
return new Promise((resolve, reject) => {
let timeout = setTimeout(() => {
elem.textContent = "Promise resolved";
resolve("resolved")
}, 2000);


signal.addEventListener('abort', () => {
elem.textContent = "Promise rejected";
clearInterval(timeout);
reject("Promise aborted")
});
});
}


function cancelPromise() {
controller.abort()
console.log(controller);
}


example(signal)
.then(data => {
console.log(data);
})
.catch(error => {
console.log("Catch: ", error)
});


document.getElementById('abort-btn').addEventListener('click', cancelPromise);


Html


<button type="button" id="abort-btn" onclick="abort()">Abort</button>
<div id="status"> </div>


注意: 需要添加填充,不支持所有浏览器。

实例

Edit elegant-lake-5jnh3

我仍在研究这个想法,但下面是我如何使用 setTimeout作为示例来实现一个可取消的承诺。

其思想是,无论何时您决定解决或拒绝一个承诺,它都会被解决或拒绝,所以它应该是一个决定何时取消的问题,满足条件,然后自己调用 reject()函数。

  • 首先,我认为提前完成一个承诺有两个原因: 完成它(我称之为 决心)和取消(我称之为 拒绝)。当然,这只是我的感觉。当然有一个 Promise.resolve()方法,但它位于构造函数本身中,并返回一个虚拟的已解析承诺。这个实例 resolve()方法实际上解析了一个实例化的承诺对象。

  • 其次,在返回新创建的承诺对象之前,您可以愉快地向它添加任何您喜欢的内容,因此我刚刚添加了 resolve()reject()方法,使它成为自包含的。

  • 第三,技巧是以后能够访问执行器 resolvereject函数,所以我只是将它们存储在闭包内的一个简单对象中。

我认为解决办法很简单,我看不出有什么大问题。

function wait(delay) {
var promise;
var timeOut;
var executor={};
promise=new Promise(function(resolve,reject) {
console.log(`Started`);
executor={resolve,reject};  //  Store the resolve and reject methods
timeOut=setTimeout(function(){
console.log(`Timed Out`);
resolve();
},delay);
});
//  Implement your own resolve methods,
//  then access the stored methods
promise.reject=function() {
console.log(`Cancelled`);
clearTimeout(timeOut);
executor.reject();
};
promise.resolve=function() {
console.log(`Finished`);
clearTimeout(timeOut);
executor.resolve();
};
return promise;
}


var promise;
document.querySelector('button#start').onclick=()=>{
promise=wait(5000);
promise
.then(()=>console.log('I have finished'))
.catch(()=>console.log('or not'));
};
document.querySelector('button#cancel').onclick=()=>{ promise.reject(); }
document.querySelector('button#finish').onclick=()=>{ promise.resolve(); }
<button id="start">Start</button>
<button id="cancel">Cancel</button>
<button id="finish">Finish</button>

如果您的代码放在一个类中,您可以使用一个装饰器。你有这样的装饰在 工具-装饰工(npm install --save utils-decorators)。如果在解析前一次调用之前,对该特定方法进行了另一次调用,则它将取消对该修饰方法的前一次调用。

import {cancelPrevious} from 'utils-decorators';


class SomeService {


@cancelPrevious()
doSomeAsync(): Promise<any> {
....
}
}

或者你可以用一个包装函式:

import {cancelPreviousify} from 'utils-decorators';


const cancelable = cancelPreviousify(originalMethod)

Https://github.com/vlio20/utils-decorators#cancelprevious-method

使用 C 承诺套餐,我们可以使用以下方法(现场演示)

import CPromise from "c-promise2";


const chain = new CPromise((resolve, reject, { onCancel }) => {
const timer = setTimeout(resolve, 1000, 123);
onCancel(() => clearTimeout(timer));
})
.then((value) => value + 1)
.then(
(value) => console.log(`Done: ${value}`),
(err, scope) => {
console.warn(err); // CanceledError: canceled
console.log(`isCanceled: ${scope.isCanceled}`); // true
}
);


setTimeout(() => {
chain.cancel();
}, 100);

同样的事情使用中止控制器(现场演示)

import CPromise from "c-promise2";


const controller= new CPromise.AbortController();


new CPromise((resolve, reject, { onCancel }) => {
const timer = setTimeout(resolve, 1000, 123);
onCancel(() => clearTimeout(timer));
})
.then((value) => value + 1)
.then(
(value) => console.log(`Done: ${value}`),
(err, scope) => {
console.warn(err);
console.log(`isCanceled: ${scope.isCanceled}`);
}
).listen(controller.signal);


setTimeout(() => {
controller.abort();
}, 100);