问题
I'm toying with promises in JavaScript and tried to promisify setTimeout function:
function timeout(ms) {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('timeout done');
}, ms);
});
}
var myPromise=timeout(3000);
myPromise.then(function(result) {
console.log(result); // timeout done
})
Fairly straightforward but I was wondering how would I go about canceling my timeout before the promise resolves. timeout
returns Promise
object hence I loose access to value that setTimeout
returns and cannot cancel timeout via clearTimeout
. What woud be the best way to do it?
BTW there is no real purpose for this, I just wonder how this would be approached. Also I plunked it here http://plnkr.co/edit/NXFjs1dXWVFNEOeCV1BA?p=preview
回答1:
What you can do it that, you can return a canceller from your timeout
function and invoke it when needed. This way you do not need to store the timeoutid
globally (or on the outer scope) and also this can manage multiple calls to the function as well. Each instance of the object return by the function timeout
will have its own canceler that can perform the cancellation.
function timeout(ms) {
var timeout, promise;
promise = new Promise(function(resolve, reject) {
timeout = setTimeout(function() {
resolve('timeout done');
}, ms);
});
return {
promise:promise,
cancel:function(){clearTimeout(timeout );} //return a canceller as well
};
}
var timeOutObj =timeout(3000);
timeOutObj.promise.then(function(result) {
console.log(result); // timeout done
});
//Cancel it.
timeOutObj.cancel();
Plnkr
回答2:
PSL's answer is right, however - there are a few caveats and I'd do it a bit differently.
- A timeout being cleared means the code will not run - so we should reject the promise.
- Returning two things isn't necessary in our case, we can monkey patch in JavaScript.
Here:
function timeout(ms, value) {
var p = new Promise(function(resolve, reject) {
p._timeout = setTimeout(function() { resolve(value); }, ms);
p.cancel = function(err){
reject(err || new Error("Timeout"));
clearTimeout(p._timeout); // We actually don't need to do this since we
// rejected - but it's well mannered to do so
});
return p;
}
Which would let us do:
var p = timeout(1500)
p.then(function(){
console.log("This will never log");
})
p.catch(function(){
console.log("This will get logged so we can now handle timeouts!")
})
p.cancel(Error("Timed out"));
One might be interested in full blown cancellation and indeed some libraries support this directly as a feature of the library. In fact I'd dare say most do. However, this causes interference problems. Quoting KrisKowal from here:
My position on cancellation has evolved. I am now convinced that cancellation (bg: that propagates) is inherently impossible with the Promise abstraction because promises can multiple dependess and dependees can be introduced at any time. If any dependee cancels a promise, it would be able to interfere with future dependees. There are two ways to get around the problem. One is to introduce a separate cancellation "capability", perhaps passed as an argument. The other is to introduce a new abstraction, a perhaps thenable "Task", which in exchange for requiring that each task only have one observer (one then call, ever), can be canceled without fear of interference. Tasks would support a fork() method to create a new task, allowing another dependee to retain the task or postpone cancellation.
回答3:
The above to answers by @Benjamin and @PSL work, but what if you need the cancelable timeout to be used by an outside source while being canceled internally?
For example, the interaction might look somewhat like this:
// externally usage of timeout
async function() {
await timeout() // timeout promise
}
// internal handling of timeout
timeout.cancel()
I needed this kind of implementation myself, so here's what I came up with:
/**
* Cancelable Timer hack.
*
* @notes
* - Super() does not have `this` context so we have to create the timer
* via a factory function and use closures for the cancelation data.
* - Methods outside the consctutor do not persist with the extended
* promise object so we have to declare them via `this`.
* @constructor Timer
*/
function createTimer(duration) {
let timerId, endTimer
class Timer extends Promise {
constructor(duration) {
// Promise Construction
super(resolve => {
endTimer = resolve
timerId = setTimeout(endTimer, duration)
})
// Timer Cancelation
this.isCanceled = false
this.cancel = function() {
endTimer()
clearTimeout(timerId)
this.isCanceled = true
}
}
}
return new Timer(duration)
}
Now you can use the timer like this:
let timeout = createTimer(100)
And have the promise canceled somewhere else:
if (typeof promise !== 'undefined' && typeof promise.cancel === 'function') {
timeout.cancel()
}
回答4:
This is my in ts:
private sleep(ms) {
let timerId, endTimer;
class TimedPromise extends Promise<any> {
isCanceled: boolean = false;
cancel = () => {
endTimer();
clearTimeout(timerId);
this.isCanceled = true;
};
constructor(fn) {
super(fn);
}
}
return new TimedPromise(resolve => {
endTimer = resolve;
timerId = setTimeout(endTimer, ms);
});
}
Usage:
const wait = sleep(10*1000);
setTimeout(() => { wait.cancel() },5 * 1000);
await wait;
来源:https://stackoverflow.com/questions/25345701/how-to-cancel-timeout-inside-of-javascript-promise