在setTimeout()中如何查找剩余时间?

105
我正在编写一些与我无法拥有并且无法(合理地)更改的库代码交互的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 );

以下是我的代码:

// setTimeout的包装函数
function mySetTimeout( func, timeout ) {
    timeouts[ n = setTimeout( func, timeout ) ] = {
        start: new Date().getTime(),
        end: new Date().getTime() + timeout
        t: timeout
    }
    return n;
}

除了IE 6以外的任何浏览器都可以正常运行,即使在我预计会发生异步操作的原始iPhone上也是如此。

15个回答

70

仅供参考,有一种方法可以在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);
}

输出:

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

但这似乎在Firefox中不起作用,但由于node.js是JavaScript,我认为这个评论可能对寻找node解决方案的人有帮助。


5
不确定是新版的Node还是其他什么原因,但为了使其正常运行,我必须删除“getTime()”部分。看起来现在_idelStart已经是一个整数了。 - kasztelan
3
我刚刚在node 0.10中尝试了一下,getTime()已被删除,_idleStart已经是时间。 - Tan Nguyen
2
无法在Node 6.3上工作,因为超时结构缺少这些信息。 - Huan
1
这个能在原生JS中运行吗,不是在Node中? - CodeBy

63

编辑:我认为我做了一个更好的版本:https://stackoverflow.com/a/36389263/2378102

我写了这个函数,而且我经常使用它:

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

创建一个计时器:
a = new timer(function() {
    // What ever
}, 3000)

所以,如果你想要剩余时间,只需要执行以下操作:
a.getTimeLeft()

谢谢。虽然不完全是我在找的,但计算剩余时间的函数非常有用。 - instead
刚刚在寻找类似的东西时发现了这个。不过在添加“停止”功能时遇到了一些困难 - 比如说,如果有一个计时器在运行,我想要完全停止/清除计时器。 - undefined

31
如果您无法修改库代码,您需要重新定义setTimeout以适应您的需求。以下是一个示例:
(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);

这不是生产代码,但它应该能让您找到正确的方向。请注意,每个超时只能绑定一个侦听器。我没有对此进行广泛测试,但在Firebug中可以正常工作。

更健壮的解决方案将使用相同的setTimeout包装技术,但是改用从返回的timeoutId到侦听器的映射来处理每个超时的多个侦听器。您还可以考虑包装clearTimeout,以便在超时被清除时分离您的侦听器。


16
由于执行时间的影响,它可能会在一段时间内漂移几毫秒。更安全的方法是记录超时开始时的时间(new Date()),然后从当前时间中减去它(另一个 new Date())。 - Jonathan

8

Node.js 服务器端特定

以上方法均未能解决我的问题。在检查超时对象后,我发现所有时间都是相对于进程启动时间的。以下方法适用于我:

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

输出:

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

node -v
v9.11.1

由于node处理进程的方式,这些代码都不会非常精确,但基本功能是给出执行或自执行以来的大致时间。正如其他人提到的那样,如果我想禁止在少于1分钟之前运行的请求,并且我存储了计时器,我认为这将作为一个快速检查而工作。在10.2+中使用refreshtimer操作对象可能会很有趣。


我得到的只有:找不到名称“process”。 - Eric

4

Javascript的事件堆栈并不像你想象的那样运作。

当一个超时事件被创建时,它被添加到事件队列中,但是其他事件可能会在该事件被触发时优先处理,延迟执行时间并推迟运行。

例如:您创建了一个10秒延迟的超时事件来向屏幕提示某些内容。它将被添加到事件堆栈中,并且将在所有当前事件被触发后执行(导致一些延迟)。然后,在处理超时事件时,浏览器仍然继续捕获其他事件并将它们添加到堆栈中,这会导致进一步的处理延迟。如果用户单击或进行大量的ctrl+typing操作,则其事件将优先于当前堆栈。你的10秒可能会变成15秒或更长。


话虽如此,有许多方法可以伪造经过的时间。其中一种方法是在将setTimeout添加到堆栈后立即执行一个setInterval。

例如:执行一个10秒延迟的setTimeout(将该延迟存储在全局变量中)。然后执行一个每秒运行一次的setInterval来从延迟中减去1并输出剩余的延迟时间。由于事件堆栈如何影响实际时间(如上所述),这仍然不准确,但可以提供一个计数。


简而言之,没有真正的方法来获取剩余时间。只有尝试向用户传达估计值的方法。


4
更快、更简单的方法:
tmo = 1000;
start = performance.now();
setTimeout(function(){
    foo();
},tmo);

您可以使用以下代码获取剩余时间:

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

1
你可以修改 setTimeout 函数,将每个超时的结束时间存储在映射中,并创建一个名为 getTimeout 的函数,以获取具有特定ID的超时剩余时间。
这是 supersolution,但我对其进行了修改,以使用稍少的内存。
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;
    }
})();

使用方法:

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

这是一个压缩版的getTimeoutIIFE

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

我希望这对您和我一样有用! :)

1

我来这里寻找答案,但是过于纠结我的问题。如果你在这里是因为你只需要在setTimeout执行期间跟踪时间,这里还有另一种方法:

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

0
问题已经得到回答,但我会补充一点。这刚刚发生在我身上。
在递归中使用setTimeout,如下所示:
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");
}

我想代码本身已经很清楚了。


0

请检查这个:

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

制作一个计时器:
let smtg = new Timer(()=>{do()}, 3000})

获取余数:

smth.get()

清除超时

smth.clear()

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接