可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
Is there a method for clearing the .then
s of a JavaScript Promise
instance?
I\'ve written a JavaScript test framework on top of QUnit. The framework runs tests synchronously by running each one in a Promise
. (Sorry for the length of this code block. I commented it as best I can, so it feels less tedious.)
/* 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);
});
});
}
If a test times out, my timeout Promise will assert.fail()
on the test so that the test is marked as failed, which is all well and good, but the test continues to run because the test Promise (result
) is still waiting to resolve it.
I need a good way to cancel my test. I can do it by creating a field on the framework module this.cancelTest
or something, and checking every so often (e.g. at the beginning of each then()
iteration) within the test whether to cancel out. However, ideally, I could use $$(at).on(\"timeout\", /* something here */)
to clear the remaining then()
s on my result
variable, so that none of the rest of the test is run.
Does something like this exist?
Quick Update
I tried using Promise.race([result, at.promise])
. It didn\'t work.
Update 2 + confusion
To unblock me, I added a few lines with the mod.cancelTest
/polling within the test idea. (I also removed the event trigger.)
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);
});
I set a breakpoint in the catch
statement, and it\'s being hit. What\'s confusing me now is that the then()
statement isn\'t being called. Ideas?
Update 3
Figured the last thing out. fn.call()
was throwing an error which I didn\'t catch, so the test promise was rejecting before at.promise.catch()
could resolve it.
回答1:
Is there a method for clearing the .then
s of a JavaScript Promise instance?
No. Not in ECMAScript 6 at least. Promises (and their then
handlers) are uncancellable by default (unfortunately). There is a bit of discussion on es-discuss (e.g. here) about how to do this in the right way, but whatever approach will win it won\'t land in ES6.
The current standpoint is that subclassing will allow to create cancellable promises using your own implementation (not sure how well that\'ll work).
Until the language commitee has figured out the best way (ES7 hopefully?) you can still use userland Promise implementations, many of which feature cancellation.
Current discussion is in the https://github.com/domenic/cancelable-promise and https://github.com/bergus/promise-cancellation drafts.
回答2:
While there isn\'t a standard way of doing this in ES6, there is a library called Bluebird to handle this.
There is also a recommended way described as part of the react documentation. It looks similar to what you have in your 2 and 3rd updates.
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
Taken from: https://facebook.github.io/react/blog/2015/12/16/ismounted-antipattern.html
回答3:
I\'m really surprised that no-one mentions Promise.race
as a candidate for this:
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 });
回答4:
simple version:
just give out the reject function.
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();}
})
}
a wraper solution (factory)
the solution I found is to pass a cancel_holder object. it will have a cancel function. if it has a cancel function then it is cancelable.
This cancel function rejects the promise with Error(\'canceled\').
Before resolve, reject, or on_cancel prevent the cancel function to be called without reason.
I have found convenient to pass the cancel action by injection
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\')
})
回答5:
If you want to stop all thens/catchs from being executed you can do this by injecting a promise that will never resolve. It probably has memory leak reprocusions but it will fix the issue and shouldn\'t cause too much wasted memory in most applications.
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
回答6:
Here\'s our implementation https://github.com/permettez-moi-de-construire/cancellable-promise
Used like
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
})
which :
- Doesn\'t touch Promise API
- Let us make further cancellation inside
catch
call
- Rely on cancellation being rejected instead of resolved unlike any other proposal or implementation
Pulls and comments welcome
回答7:
Set a \"cancelled\" property on the Promise to signal then()
and catch()
to exit early. It\'s very effective, especially in Web Workers that have existing microtasks queued up in Promises from onmessage
handlers.
// 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;
}
回答8:
@Michael Yagudaev \'s answer works for me.
But the original answer did not chain the wrapped promise with .catch() to handle reject handling, here is my improvement on top of @Michael Yagudaev\'s answer:
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();
回答9:
There are a few npm libraries for cancellable promises.
p-cancelable
https://github.com/sindresorhus/p-cancelable
cancelable-promise
https://github.com/alkemics/CancelablePromise
回答10:
If p is a variable that contains a Promise, then p.then(empty);
should dismiss the promise when it eventually completes or if it is already complete (yes, I know this isn\'t the original question, but it is my question). \"empty\" is function empty() {}
. I\'m just a beginner and probably wrong, but these other answers seem too complicated. Promises are supposed to be simple.