如何在JavaScript循环中添加延迟?

501

我想在一个while循环内添加延迟/休眠:

我尝试像这样实现:

alert('hi');

for(var start = 1; start < 10; start++) {
  setTimeout(function () {
    alert('hello');
  }, 3000);
}

只有第一种情况是正确的:显示 alert('hi') 后,它将等待3秒钟,然后显示 alert('hello'),但随后 alert('hello') 将不断地重复显示。

我希望的是,在显示 alert('hi') 之后3秒钟后显示 alert('hello'),然后再等待3秒钟显示第二次 alert('hello'),以此类推。


for(var i=0; i < 5; i++){ delayLoop(i) }; function delayLoop(i){ setTimeout(function(){ console.log('每隔1秒打印一次', (i*1000)) } } - mhndlsz
const setTimeOutFn = async() => { for (var start = 0; start < 3; start++) { await new Promise(async(res, rej) => { setTimeout(() => { console.log('你好', start); res() }, 3000); }) } } - Bilal Khursheed
在每个循环中设置不同值的超时可能不是一个好主意。以下是一种使用Promise(async / await)实际上通过无限期挂起代码执行的无聊单行程序:https://dev59.com/DHA65IYBdhLWcg3w4C0L#73588338 - marko-36
33个回答

965

setInterval()函数是非阻塞的并且会立即返回。因此,你的循环将很快迭代,并且它将依次在短时间内触发3秒的延迟。这就是为什么你的第一个警报框在3秒后弹出,所有其余的都会依次跟随没有延迟。

你可能需要使用类似于以下内容:

var i = 1;                  //  set your counter to 1

function myLoop() {         //  create a loop function
  setTimeout(function() {   //  call a 3s setTimeout when the loop is called
    console.log('hello');   //  your code here
    i++;                    //  increment the counter
    if (i < 10) {           //  if the counter < 10, call the loop function
      myLoop();             //  ..  again which will trigger another 
    }                       //  ..  setTimeout()
  }, 3000)
}

myLoop();                   //  start the loop

你也可以使用自执行函数,并将迭代次数作为参数传递来使代码更加简洁:

(function myLoop(i) {
  setTimeout(function() {
    console.log('hello'); //  your code here                
    if (--i) myLoop(i);   //  decrement i and call myLoop again if i > 0
  }, 3000)
})(10);                   //  pass the number of iterations as an argument


47
使用递归来实现这个功能是否最终会导致堆栈溢出?如果要执行一百万次迭代,有更好的实现方法吗?像下面Abel的解决方案,使用setInterval然后清除它,可能更好一些。 - Adam
14
我理解的是,由于setTimeout是非阻塞的,所以这不是递归——每个setTimeout之后堆栈窗口都会关闭,而且只有一个setTimeout在等待执行……对吗? - Joe
4
当使用像for in循环这样迭代对象的操作时,这会怎样运作? - vsync
1
@vsync 请查看 Object.keys() - Braden Best
1
@Adam 你在stackoverflow.com上谈论递归导致堆栈溢出。你赢得了互联网! - deltaray
显示剩余8条评论

306

自从 ES7 开始,await 循环的方法更好了:

// Returns a Promise that resolves after "ms" Milliseconds
const timer = ms => new Promise(res => setTimeout(res, ms))

async function load () { // We need to wrap the loop into an async function for this to work
  for (var i = 0; i < 3; i++) {
    console.log(i);
    await timer(3000); // then the created Promise can be awaited
  }
}

load();
当引擎到达await部分时,它会设置一个超时并暂停async函数的执行。然后当超时完成时,执行将在那个点继续。这非常有用,因为您可以延迟(1)嵌套循环,(2)条件性地,(3)嵌套函数:

async function task(i) { // 3
  await timer(1000);
  console.log(`Task ${i} done!`);
}

async function main() {
  for(let i = 0; i < 100; i+= 10) {
    for(let j = 0; j < 10; j++) { // 1
      if(j % 2) { // 2
        await task(i + j);
      }
    }
  }
}
    
main();

function timer(ms) { return new Promise(res => setTimeout(res, ms)); }

MDN 上的参考资料

虽然 ES7 已经被 NodeJS 和现代浏览器支持,但你可能想要使用 BabelJS 转译它以便在任何地方运行。


1
@sachin break; 可能吗? - Jonas Wilms
1
谢谢您提供的解决方案。使用所有现有的控制结构,而不需要发明 Continuations 确实是非常好的。 - Gus
这仍然只会创建各种计时器,它们将在不同的时间解决而不是按顺序? - David Yell
17
这绝对是最好的解决方案,应该被采纳作为答案。被采纳的答案不可靠,不建议使用。 - AlphaHowl
3
是个不错的解决方案,但我想挑刺一下,我会把函数称为 sleep 或者 wait 而不是 timer。类是名词,函数是动词。它们做某些事情或采取某些行动,而不是代表某个东西。 - ggorlen
显示剩余2条评论

127

如果使用ES6,您可以使用for循环来实现此操作:

for (let i = 1; i < 10; i++) {
  setTimeout(function timer() {
    console.log("hello world");
  }, i * 3000);
}

它声明 i 用于每个迭代,这意味着超时时间是之前的时间 + 1000。这样,传递给setTimeout的就是我们想要的东西。


3
我认为这与https://dev59.com/DHA65IYBdhLWcg3w4C0L#3583795中所描述的答案一样存在内存分配问题。 - Flame_Phoenix
2
@Flame_Phoenix 什么内存分配问题? - 4castle
3
setTimeout调用在循环内同步计算出i*3000参数的值,并按照其值传递给setTimeout。使用let是可选的且与问题和答案无关。 - traktor
2
@Flame_Phoenix提到了这段代码存在问题。基本上,在第一次循环中,您创建了一个计时器,然后立即重复循环,直到循环结束条件(i < 10)满足,因此您将有多个计时器并行工作,这会导致内存分配问题,并且在更大的迭代量下情况会更糟。 - XCanG
1
@traktor53 是完全正确的,只有在您计划在 setTimeout 回调中使用它时,使用 let 才更优越。您可能希望在您的答案中指出这一点。 - Jonas Wilms

82

试试像这样:

var i = 0, howManyTimes = 10;

function f() {
  console.log("hi");
  i++;
  if (i < howManyTimes) {
    setTimeout(f, 3000);
  }
}

f();


const run = (t, d) => {console.log(t); t > 1 && setTimeout(run, d, --t, d)} - vsync
我喜欢这个答案,因为它有效地创建了一个同步循环,并且最容易理解。 - Vincent

25

另一种方式是将超时时间乘以一个因子,但请注意这与sleep不同。循环后的代码将立即执行,只有回调函数的执行被延迟。

for (var start = 1; start < 10; start++)
    setTimeout(function () { alert('hello');  }, 3000 * start);

第一个超时时间将被设置为3000 * 1,第二个超时时间将被设置为3000 * 2,以此类推。


3
值得指出的是,使用这种方法在函数内部无法可靠地使用 start - DBS
2
不良实践 - 不必要的内存分配。 - Alexander Trakhimenok
点赞鼓励创造力,但这是非常糟糕的实践。 :) - Salivan
2
为什么这是一种不好的编程实践?它有内存分配问题吗? 这个回答是否遇到相同的问题?https://dev59.com/DHA65IYBdhLWcg3w4C0L#36018502 - Flame_Phoenix
1
@Flame_Phoenix 这是不好的编程实践,因为程序将为每个循环保留一个计时器,所有计时器同时运行。因此,如果有1000次迭代,在开始时将有1000个计时器同时运行。 - Joakim
除了是不好的实践外,这实际上是错误的行为,也没有回答问题。它不会在循环中“等待”,也不会在alert()被清除后再添加3秒钟。它会在3秒钟(加上一些变化)的间隔内设置alert - niry

20

这会起作用

for (var i = 0; i < 10; i++) {
  (function(i) {
    setTimeout(function() { console.log(i); }, 100 * i);
  })(i);
}

试试这个代码片段:https://jsfiddle.net/wgdx8zqq/


1
这确实会在相同的时间触发所有超时调用。 - Eddie
我只想说,我已经破解了这种方式,使用了$.Deferred,但是它需要一些不同的情况才能让它工作,向你致敬..! - ArifMustafa

20
您可以创建一个将setTimeout转换为Promise的sleep函数。 这使您可以使用async/await编写没有回调和熟悉的for循环控制流的代码。

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

(async () => {
  for (let i = 0; i < 10; i++) {
    console.log(i);
    await sleep(1000);
  }

  console.log("done");
})();

在Node中,您可以使用timers/promises来避免promisification步骤(如果该功能在您的旧Node版本上不受支持,则上述代码同样有效):

const {setTimeout: sleep} = require("timers/promises");

// same code as above

然而,由于JS是单线程的,timeout异步执行非常有益。如果不这样做,浏览器就无法获得重绘UI的机会,导致用户界面冻结。


18

我认为你需要类似于这样的东西:

var TimedQueue = function(defaultDelay){
    this.queue = [];
    this.index = 0;
    this.defaultDelay = defaultDelay || 3000;
};

TimedQueue.prototype = {
    add: function(fn, delay){
        this.queue.push({
            fn: fn,
            delay: delay
        });
    },
    run: function(index){
        (index || index === 0) && (this.index = index);
        this.next();
    },
    next: function(){
        var self = this
        , i = this.index++
        , at = this.queue[i]
        , next = this.queue[this.index]
        if(!at) return;
        at.fn();
        next && setTimeout(function(){
            self.next();
        }, next.delay||this.defaultDelay);
    },
    reset: function(){
        this.index = 0;
    }
}

测试代码:

var now = +new Date();

var x = new TimedQueue(2000);

x.add(function(){
    console.log('hey');
    console.log(+new Date() - now);
});
x.add(function(){
    console.log('ho');
    console.log(+new Date() - now);
}, 3000);
x.add(function(){
    console.log('bye');
    console.log(+new Date() - now);
});

x.run();

注意:使用警告框会阻止 JavaScript 执行,直到您关闭了警告框。

可能会比您要求的代码更多,但这是一个健壮的可重复使用的解决方案。


16

我可能会使用setInterval,像这样:

var period = 1000; // ms
var endTime = 10000;  // ms
var counter = 0;
var sleepyAlert = setInterval(function(){
    alert('Hello');
    if(counter === endTime){
       clearInterval(sleepyAlert);
    }
    counter += period;
}, period);

3
SetTimeout比setInterval更好。谷歌一下就知道了。 - Airy
17
我在谷歌上做了一些搜索,但没有找到任何信息。为什么setInterval不好?你能给我们提供一个链接或者一个例子吗?谢谢。 - Marcs
我猜这篇文章的观点是,即使出现错误或阻塞,SetInterval()仍会不断生成“线程”。 - Mateen Ulhaq

13

在我看来,向循环中添加延迟的最简单和最优雅的方法是这样的:

names = ['John', 'Ana', 'Mary'];

names.forEach((name, i) => {
 setTimeout(() => {
  console.log(name);
 }, i * 1000);  // one sec interval
});

1
这实际上很糟糕。forEach 不会等待你的 setTimeout。你只是为数组中的每个元素创建了许多超时。对于一个有 3 个元素的数组,这还可以接受。但如果你有数百甚至数千个元素,你的电脑可能会着火。 - Omar Omeiri

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