如何停止一个 setTimeout 循环?

94

我正在尝试使用图像精灵构建一个加载指示器,并编写了以下函数:

function setBgPosition() {
   var c = 0;
    var numbers = [0, -120, -240, -360, -480, -600, -720];
    function run() {
       Ext.get('common-spinner').setStyle('background-position', numbers[c++] + 'px 0px');
        if (c<numbers.length)
        {
            setTimeout(run, 200);
        }else
        {
            setBgPosition();
        }
    }
    setTimeout(run, 200);
}
所以输出结果看起来像这样: http://jsfiddle.net/TTkre/ 我不得不在else语句中使用setBgPosition()来使其循环运行,所以现在我的问题是如何在想要[加载完成]时停止此循环?

当 if (c<numbers.length) 为真时,您想停止循环吗? - Hemant Metalia
如果我不使用else,动画只会执行一次。 - Gihan Lasita
10个回答

135
setTimeout函数返回一个计时器句柄,你可以使用该句柄通过clearTimeout来取消对应的计时器。
例如:
function setBgPosition() {
    var c = 0,
        timer = 0;
    var numbers = [0, -120, -240, -360, -480, -600, -720];
    function run() {
        Ext.get('common-spinner').setStyle('background-position', numbers[c++] + 'px 0px');
        if (c >= numbers.length) {
            c = 0;
        }
        timer = setTimeout(run, 200);
    }
    timer = setTimeout(run, 200);

    return stop;

    function stop() {
        if (timer) {
            clearTimeout(timer);
            timer = 0;
        }
}

因此,你可以这样使用:

var stop = setBgPosition();
// ...later, when you're ready to stop...
stop();
请注意,我没有让setBgPosition再次调用自己,而是让它将c设置回0。否则,这样做是行不通的。还要注意,我使用了0作为超时未挂起时的句柄值;0不是setTimeout的有效返回值,因此它是一个方便的标记。
在我看来,这也是你最好使用setInterval而不是setTimeout的(少数)地方之一。 setInterval会重复执行。所以:
function setBgPosition() {
    var c = 0;
    var numbers = [0, -120, -240, -360, -480, -600, -720];
    function run() {
        Ext.get('common-spinner').setStyle('background-position', numbers[c++] + 'px 0px');
        if (c >= numbers.length) {
            c = 0;
        }
    }
    return setInterval(run, 200);
}

这样使用:

var timer = setBgPosition();
// ...later, when you're ready to stop...
clearInterval(timer);
尽管如上所述,我想找到一种方法通过检测满足某些完成条件来使setBgPosition自己停止。

如果我想在函数运行时传递一个变量怎么办? - Master Yoda
@BhawinParkeria:你可以像这样将其包装在函数中:setInterval(function() { run(argument); }, 200);,或者使用 Function#bind,像这样:setInterval(run.bind(null, argument), 200);。前者在每次运行时需要查看参数的当前值时非常有用,后者则在您希望“烘焙”argument的当前值并保持使用即使argument稍后更改时非常有用。 - T.J. Crowder
如果我使用 setInterval(function() { run(argument); }, 200);,我能够使用 clearTimeout 吗? - Master Yoda
@BhawinParkeria:是的,为什么不呢?句柄与您传递给setInterval的函数无关。(我会使用clearInterval而不是clearTimeout,但它们将执行相同的操作。) - T.J. Crowder
2
@philosopher - 你不需要 if。:-) 我以前总是把它放在那里,但是 0 现在被明确定义为无效的计时器句柄,而 clearTimeout 被明确定义为在无效的计时器句柄上不会抛出任何错误或类似的东西,所以你可以摆脱那个 if 并保持主体不变。(现在我也这样做了。) 关于 run 中的 await:只需注意两件事:1. 如果在 run 中使用 await,则根据定义,run 必须是一个 async 函数,这意味着它返回一个 promise,这意味着你正在向某个东西(在 [cont'd] - T.J. Crowder
显示剩余5条评论

13

我知道这是一个老问题,但我还是想发布我的解决方法。这样你就不需要像T.J.Crowder解释的那样处理0技巧了。

var keepGoing = true;

function myLoop() {
    // ... Do something ...

    if(keepGoing) {
        setTimeout(myLoop, 1000);
    }
}

function startLoop() {
    keepGoing = true;
    myLoop();
}

function stopLoop() {
    keepGoing = false;
}

我知道这个回答很老,但是如果你在1000毫秒的超时内调用了 stopLoopstartLoop,会发生什么?难道你现在不会有两个循环同时运行,都在检查 keepGoing 是否为真吗? - Mirror318
没错,@Mirror318。setTimeout返回一个处理程序。该处理程序应使用clearTimeout清除。 - ThimoKl
这个方法是可行的,但是取消定时器本身更准确,因为你不需要等待循环完成,而在你的例子中,可能需要整整一秒钟。 - Victor Stoddard

6

处理超时循环的最简单方法

function myFunc (terminator = false) {
    if(terminator) {
        clearTimeout(timeOutVar);
    } else {
        // do something
        timeOutVar = setTimeout(function(){myFunc();}, 1000);
    }
}   
myFunc(true); //  -> start loop
myFunc(false); //  -> end loop

1
我翻译一下:我说的不对吗?逻辑反了吧? - bmiskie

4
如果您想从函数内部停止循环,请尝试以下代码:
let timer = setInterval(function(){
  // Have some code to do something

  if(/*someStopCondition*/){ 
    clearInterval(timer)
  }
},1000);

您还可以将其包装在另一个函数中,只需确保您有一个计时器变量,并使用clearInterval(theTimerVariable)来停止循环。


2
var myVar = null;

if(myVar)
   clearTimeout(myVar);

myVar = setTimeout(function(){ alert("Hello"); }, 3000);

2
这可能回答了问题。然而,仅有代码的答案并不像记录代码或对为什么这段代码是解决方案进行详细解释的答案那样有用。 - RyanNerd

2

你需要使用一个变量来跟踪“完成度”,然后在循环的每次迭代中进行测试。如果 done == true,则返回。

var done = false;

function setBgPosition() {
    if ( done ) return;
    var c = 0;
    var numbers = [0, -120, -240, -360, -480, -600, -720];
    function run() {
        if ( done ) return;
        Ext.get('common-spinner').setStyle('background-position', numbers[c++] + 'px 0px');
        if (c<numbers.length)
        {
            setTimeout(run, 200);
        }else
        {
            setBgPosition();
        }
    }
    setTimeout(run, 200);
}

setBgPosition(); // start the loop

setTimeout( function(){ done = true; }, 5000 ); // external event to stop loop

1
作为标记有extjs标签,可能值得看一下extjs方法:http://docs.sencha.com/extjs/6.2.0/classic/Ext.Function.html#method-interval 它的工作方式类似于setInterval,但也关注作用域,并允许传递参数:
function setBgPosition() {
    var c = 0;
    var numbers = [0, -120, -240, -360, -480, -600, -720];
    function run() {
       Ext.get('common-spinner').setStyle('background-position', numbers[c++] + 'px 0px');
        if (c<numbers.length){
            c=0;
        }
    }
    return Ext.Function.interval(run,200);
}

var bgPositionTimer = setBgPosition();

当你想停止时,可以使用clearInterval来停止它。
clearInterval(bgPositionTimer);

一个示例用例是:
Ext.Ajax.request({
    url: 'example.json',

    success: function(response, opts) {
        clearInterval(bgPositionTimer);
    },

    failure: function(response, opts) {
        console.log('server-side failure with status code ' + response.status);
        clearInterval(bgPositionTimer);
    }
});

0

当任务完成并且您可以显示任务(在您的情况下为图像)时,在下一次刷新时不要发送JavaScript。如果您的服务器正在使用PHP。

<?php if (!$taskCompleted) { ?>
<script language="javascript">
setTimeout(function(){
   window.location.reload(1);
}, 5000);
</script>
<?php } ?>

0

我不确定,但可能是你想要的:

var c = 0;
function setBgPosition()
{
    var numbers = [0, -120, -240, -360, -480, -600, -720];
    function run()
    {
        Ext.get('common-spinner').setStyle('background-position', numbers[c++] + 'px 0px');
        if (c<=numbers.length)
        {
            setTimeout(run, 200);
        }
        else
        {
            Ext.get('common-spinner').setStyle('background-position', numbers[0] + 'px 0px');
        }
    }
    setTimeout(run, 200);
}
setBgPosition();

0
在最佳答案中,我认为 if (timer) 语句误放在了 stop() 函数调用内部。它应该被放置在 run() 函数调用内部,像这样:if (timer) timer = setTimeout(run, 200)。这可以防止在 stop() 被调用后立即运行未来的 setTimeout 语句。 编辑 2:对于同步函数调用,最佳答案是正确的。如果您想进行异步函数调用,则使用我的代码。 以下是一个示例,我认为这是正确的方式(如果我错了,请随时纠正我,因为我还没有测试过):
const runSetTimeoutsAtIntervals = () => {
    const timeout = 1000 // setTimeout interval
    let runFutureSetTimeouts // Flag that is set based on which cycle continues or ends

    const runTimeout = async() => {
        await asyncCall() // Now even if stopRunSetTimeoutsAtIntervals() is called while this is running, the cycle will stop
        if (runFutureSetTimeouts) runFutureSetTimeouts = setTimeout(runTimeout, timeout)
    }

    const stopRunSetTimeoutsAtIntervals = () => {
        clearTimeout(runFutureSetTimeouts)
        runFutureSetTimeouts = false
    }

    runFutureSetTimeouts = setTimeout(runTimeout, timeout) // Set flag to true and start the cycle
    return stopRunSetTimeoutsAtIntervals
}

// You would use the above function like follows.
const stopRunSetTimeoutsAtIntervals = runSetTimeoutsAtIntervals() // Start cycle
stopRunSetTimeoutsAtIntervals() // Stop cycle

编辑 1:已经测试并按预期工作。


“在最佳答案中,我认为 if (timer) 语句错误地放在 stop() 函数调用内部。” 不,它放置得很正确。这是因为当您调用 stop 时,如果计时器正在运行,它会停止计时器。正如我 之前在您询问此问题时所解释的那样,在调用 clearTimeout 之前并不一定需要进行此检查(现在我不这么做了),但这并不是错误的。 - T.J. Crowder
@T.J.Crowder 感谢您的回复。您说得对,如果将 if (timer) 语句放置或移除到 stop() 函数内部都不会改变任何东西。然而,我仍然坚持我的观点,即 if 语句仍然需要放置在 run() 函数内部,例如 if (timer) timer = setTimeout(run, 200); - philosopher
为了解释这个问题,请考虑在你的代码中调用stop()时,任何在Ext.get()...c = 0;之间的代码都将被执行。在这种情况下,clearTimeout将清除先前设置的timer而不是在c = 0;行后立即执行的timer,这将导致循环继续,即使你已经调用了stop() - philosopher
如果Ext.get()...c=0;之间的行是一个await async function,那么它可能不会立即完成执行,从而导致这种情况更有可能发生。我在上面的答案中给出的代码涵盖了这种边缘情况,并防止其发生。 - philosopher
1
为了解释这个问题,请考虑在您的代码中,当在包括 Ext.get()... 到 c = 0; 之间的任何代码正在执行时,stop() 被调用的情况。但是在我的代码中不可能发生这种情况,因为 run 不是一个 async 函数。它可以在您的代码中发生,因为它是一个 async 函数。所以在您的 runTimeout 中是有意义的,但在我的 run 中却没有意义。 - T.J. Crowder
1
@T.J.Crowder 好的,想了想,你说得对,感谢讨论和澄清! :) - philosopher

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