在 setTimeout()中找到剩余的时间?

我正在编写一些 Javascript,它们与我并不拥有的库代码进行交互,并且不能(合理地)进行更改。它创建了用于在一系列有时间限制的问题中显示下一个问题的 Javascript 超时。这不是真正的代码,因为它已经混淆得无可救药了。图书馆的工作是这样的:

....
// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = setTimeout( showNextQuestion(questions[i+1]), t );

我想把一个进度条在屏幕上填充到 questionTime * 1000通过询问由 setTimeout创建的计时器。唯一的问题是,似乎没有办法做到这一点。我是不是遗漏了一个 getTimeout功能?我能找到的关于 Javascript 超时的唯一信息只与通过 setTimeout( function, time)创建和通过 clearTimeout( id )删除有关。

我正在寻找一个函数,它返回超时触发之前剩余的时间,或调用超时之后经过的时间。我的进度条形码是这样的:

var  timeleft = getTimeout( test.currentTimeout ); // I don't know how to do this
var  $bar = $('.control .bar');
while ( timeleft > 1 ) {
$bar.width(timeleft / test.defaultQuestionTime * 1000);
}

医生: 如何查找 javascript setTimeout ()之前的剩余时间?


这是我现在使用的解决方案。我浏览了负责测试的库部分,并解密了代码(非常糟糕,并且违反了我的权限)。

// setup a timeout to go to the next question based on user-supplied time
var t = questionTime * 1000
test.currentTimeout = mySetTimeout( showNextQuestion(questions[i+1]), t );

我的原则是:

// wrapper for setTimeout
function mySetTimeout( func, timeout ) {
timeouts[ n = setTimeout( func, timeout ) ] = {
start: new Date().getTime(),
end: new Date().getTime() + timeout
t: timeout
}
return n;
}

这在任何不是 IE6的浏览器中都能很好地工作。甚至是最初的 iPhone,我原以为事情会变得异步。

93211 次浏览

No, but you can have your own setTimeout/setInterval for animation in your function.

Say your question looks like this:

function myQuestion() {
// animate the progress bar for 1 sec
animate( "progressbar", 1000 );


// do the question stuff
// ...
}

And your animation will be handled by these 2 functions:

function interpolate( start, end, pos ) {
return start + ( pos * (end - start) );
}


function animate( dom, interval, delay ) {


interval = interval || 1000;
delay    = delay    || 10;


var start    = Number(new Date());


if ( typeof dom === "string" ) {
dom = document.getElementById( dom );
}


function step() {


var now     = Number(new Date()),
elapsed = now - start,
pos     = elapsed / interval,
value   = ~~interpolate( 0, 500, pos ); // 0-500px (progress bar)


dom.style.width = value + "px";


if ( elapsed < interval )
setTimeout( step, delay );
}


setTimeout( step, delay );
}

Javascript's event stacks don't operate how you would think.

When a timeout event is created, it is added to the event queue, but other events may take priority while that event is being fired, delay the execution time and postponing runtime.

Example: You create a timeout with a delay of 10 seconds to alert something to the screen. It will be added to the event stack and will be executed after all current events are fired (causing some delay). Then, when the timeout is processed, the browser still continues to capture other events add them to the stack, which causes further delays in the processing. If the user clicks, or does a lot of ctrl+typing, their events take priority over the current stack. Your 10 seconds can turn into 15 seconds, or longer.


That being said, there are many ways to fake how much time has passed. One way is to execute a setInterval right after you add the setTimeout to the stack.

Example: Perform a settimeout with a 10 second delay (store that delay in a global). Then perform a setInterval that runs every second to subtract 1 from the delay and output the delay remaining. Because of how the event stack can influence actual time (described above), this still won't be accurate, but does give a count.


In short, there is no real way to get the remaining time. There are only ways to try and convey an estimate to the user.

If you can't modify the library code, you'll need to redefine setTimeout to suit your purposes. Here's an example of what you could do:

(function () {
var nativeSetTimeout = window.setTimeout;


window.bindTimeout = function (listener, interval) {
function setTimeout(code, delay) {
var elapsed = 0,
h;


h = window.setInterval(function () {
elapsed += interval;
if (elapsed < delay) {
listener(delay - elapsed);
} else {
window.clearInterval(h);
}
}, interval);
return nativeSetTimeout(code, delay);
}


window.setTimeout = setTimeout;
setTimeout._native = nativeSetTimeout;
};
}());
window.bindTimeout(function (t) {console.log(t + "ms remaining");}, 100);
window.setTimeout(function () {console.log("All done.");}, 1000);

This is not production code, but it should put you on the right track. Note that you can only bind one listener per timeout. I haven't done extensive testing with this, but it works in Firebug.

A more robust solution would use the same technique of wrapping setTimeout, but instead use a map from the returned timeoutId to listeners to handle multiple listeners per timeout. You might also consider wrapping clearTimeout so you can detach your listener if the timeout is cleared.

Just for the record, there is a way to get the time left in node.js:

var timeout = setTimeout(function() {}, 3600 * 1000);


setInterval(function() {
console.log('Time left: '+getTimeLeft(timeout)+'s');
}, 2000);


function getTimeLeft(timeout) {
return Math.ceil((timeout._idleStart + timeout._idleTimeout - Date.now()) / 1000);
}

Prints:

$ node test.js
Time left: 3599s
Time left: 3597s
Time left: 3595s
Time left: 3593s

This doesn't seem to work in firefox through, but since node.js is javascript, I thought this remark might be helpful for people looking for the node solution.

If anyone's looking back on this. I've come out with a timeout and interval manager that can get you the time left in a timeout or interval as well as do some other stuff. I'll be adding to it to make it more nifty and more accurate, but it seems to work fairly well as is (although I have some more ideas to make it even more accurate):

https://github.com/vhmth/Tock

EDIT: I actually think I made an even better one: https://stackoverflow.com/a/36389263/2378102

I wrote this function and I use it a lot:

function timer(callback, delay) {
var id, started, remaining = delay, running


this.start = function() {
running = true
started = new Date()
id = setTimeout(callback, remaining)
}


this.pause = function() {
running = false
clearTimeout(id)
remaining -= new Date() - started
}


this.getTimeLeft = function() {
if (running) {
this.pause()
this.start()
}


return remaining
}


this.getStateRunning = function() {
return running
}


this.start()
}

Make a timer:

a = new timer(function() {
// What ever
}, 3000)

So if you want the time remaining just do:

a.getTimeLeft()

Question has already been answered but I will add my bit. It just occured to me.

Use setTimeout in recursion as follows:

var count = -1;


function beginTimer()
{
console.log("Counting 20 seconds");
count++;


if(count <20)
{
console.log(20-count+"seconds left");
setTimeout(beginTimer,2000);
}
else
{
endTimer();
}
}


function endTimer()
{
console.log("Time is finished");
}

I guess the code is self explanatory

Check this one:

class Timer {
constructor(fun,delay) {
this.timer=setTimeout(fun, delay)
this.stamp=new Date()
}
get(){return ((this.timer._idleTimeout - (new Date-this.stamp))/1000) }
clear(){return (this.stamp=null, clearTimeout(this.timer))}
}

Make a timer:

let smtg = new Timer(()=>{do()}, 3000})

Get remain:

smth.get()

Clear timeout

smth.clear()
    (function(){
window.activeCountdowns = [];
window.setCountdown = function (code, delay, callback, interval) {
var timeout = delay;
var timeoutId = setTimeout(function(){
clearCountdown(timeoutId);
return code();
}, delay);
window.activeCountdowns.push(timeoutId);
setTimeout(function countdown(){
var key = window.activeCountdowns.indexOf(timeoutId);
if (key < 0) return;
timeout -= interval;
setTimeout(countdown, interval);
return callback(timeout);
}, interval);
return timeoutId;
};
window.clearCountdown = function (timeoutId) {
clearTimeout(timeoutId);
var key = window.activeCountdowns.indexOf(timeoutId);
if (key < 0) return;
window.activeCountdowns.splice(key, 1);
};
})();


//example
var t = setCountdown(function () {
console.log('done');
}, 15000, function (i) {
console.log(i / 1000);
}, 1000);

Server side Node.js specific

None of the above really worked for me, and after inspecting the timeout object it looked like everything was relative to when the process started. The following worked for me:

myTimer = setTimeout(function a(){console.log('Timer executed')},15000);


function getTimeLeft(timeout){
console.log(Math.ceil((timeout._idleStart + timeout._idleTimeout)/1000 - process.uptime()));
}


setInterval(getTimeLeft,1000,myTimer);

Output:

14
...
3
2
1
Timer executed
-0
-1
...


node -v
v9.11.1

Edited output for brevity, but this basic function gives a approximate time until execution or since execution. As others mention, none of this will be exact due to the way node processes, but if I want to suppress a request that was run less than 1 minute ago, and I stored the timer, I don't see why this wouldn't work as a quick check. Could be interesting to juggle objects with refreshtimer in 10.2+.

I stopped by here looking for this answer, but was overthinking my problem. If you are here because you just need to keep track of time while you're setTimeout is in progress, here's another way to do it:

    var focusTime = parseInt(msg.time) * 1000


setTimeout(function() {
alert('Nice Job Heres 5 Schrute bucks')
clearInterval(timerInterval)
}, focusTime)


var timerInterval = setInterval(function(){
focusTime -= 1000
initTimer(focusTime / 1000)
}, 1000);

You can modify setTimeout to store each timeout's end time in a map and create a function called getTimeout to get the time left for a timeout with a certain id.

This was super's solution, but I modified it to use slightly less memory

let getTimeout = (() => { // IIFE
let _setTimeout = setTimeout, // Reference to the original setTimeout
map = {}; // Map of all timeouts with their end times


setTimeout = (callback, delay) => { // Modify setTimeout
let id = _setTimeout(callback, delay); // Run the original, and store the id
map[id] = Date.now() + delay; // Store the end time
return id; // Return the id
};


return (id) => { // The actual getTimeout function
// If there was no timeout with that id, return NaN, otherwise, return the time left clamped to 0
return map[id] ? Math.max(map[id] - Date.now(), 0) : NaN;
}
})();

Usage:

// go home in 4 seconds
let redirectTimeout = setTimeout(() => {
window.location.href = "/index.html";
}, 4000);


// display the time left until the redirect
setInterval(() => {
document.querySelector("#countdown").innerHTML = `Time left until redirect ${getTimeout(redirectTimeout)}`;
},1);

Here's a minified version of this getTimeout IIFE:

let getTimeout=(()=>{let t=setTimeout,e={};return setTimeout=((a,o)=>{let u=t(a,o);return e[u]=Date.now()+o,u}),t=>e[t]?Math.max(e[t]-Date.now(),0):NaN})();

I hope this is as useful to you as it was for me! :)

A quicker, easier way:

tmo = 1000;
start = performance.now();
setTimeout(function(){
foo();
},tmo);

You can get the time remaining with:

 timeLeft = tmo - (performance.now() - start);