JavaScript:延迟循环遍历数组

11

我想循环遍历一个数组,但是希望每个数组的值都有延迟输出。这是我目前对它应该如何工作的理解:

编辑

请求的JS Fiddle:http://jsfiddle.net/d3whkjww/

    loopThroughSplittedText: function(splittedText) {

        for (var i = 0; i < splittedText.length; i++) {
            // for each iteration console.log a word
            // and make a pause after it
            setTimeout(
                console.log(splittedText[i]),
                1000
            );
        };

    },

然而,它并没有起作用,我认为这可能是因为"for"循环中的参数必须在setTimeout函数内部。但我不知道怎样使它起作用。

现在我得到的是数组的每个值都一次性出现,但我希望它们能够延迟显示。我该怎么做?


你能提供一个Fiddle给我们吗,这样人们就可以用你的数据进行测试了吗? - YaBCK
你需要增加超时的“duration”。 - Daniel A. White
@Jamiec:我认为重复的问题应该是关于在提供的代码中使用setTimeout的问题,但似乎OP立即调用而不是将函数作为第一个参数传递。 - Qantas 94 Heavy
我已经查看了您的Fiddle,并为您使用您的数组制作了一个示例。在我的示例中,它将向您展示如何使用计时器创建数组。这很好,因为您可以在代码的其他部分中使用计时器,而无需不断输入代码。 - YaBCK
13个回答

13
var splittedText = ["Hello", "World", "How", "Are", "You", "Today"];

function loopThroughSplittedText(splittedText) {
    for (var i = 0; i < splittedText.length; i++) {
        // for each iteration console.log a word
        // and make a pause after it
        (function (i) {
            setTimeout(function () {
                document.getElementById('text').innerHTML += splittedText[i];
                console.log(splittedText[i]);
            }, 1000 * i);
        })(i);
    };
}
loopThroughSplittedText(splittedText);

演示链接


8

很可能你想在这里使用递归函数而不是for循环。但是,我将解释两种方法,以防你(或其他人)坚持要用循环来完成。

对于递归函数,一般想法是调用函数一次,然后让它重复调用自身,直到完成所需操作。在代码方面,它可能看起来有点像这样:

loopThroughSplittedText: function(splittedText) {

  // Create our counter; delayedOutput will use this to
  // track how far along in our string we are currently at
  var locationInString = 0;

  function delayedOutput() {

    // Output the next letter in our string
    console.log(splittedText[locationInString]);

    // Increment our counter so that on the next call we are on the next letter
    locationInString++;

    // Only perform setTimeout if we still have text left to output
    if (locationInString < splittedText.length) {

      // Functions can reference themselves using their own name
      setTimeout(delayedOutput, 1000);
    }
  }

  // Call our function once to get things started
  delayedOutput(); 
},

或者,如果你真的喜欢使用循环,你仍然可以这样做,但是需要进行一些调整才能实现。

首先,你需要将console.log放在它自己的函数中。这是因为当你放置console.log(something)时,你实际上并没有传递它,而是立即调用它,这不是你想要的;通过调用它,它会立即将文本输出到控制台,而不是等待稍后。将其放入自己的函数中允许它被传递给setTimeout,以便稍后调用。

其次,你需要将那个函数包装在另一个函数中,以确保它在触发时得到正确的i值。原因实际上是这样的:你的意图是告诉函数“当你准备好了,使用我设置你时的i值。”但是,你现在正在做的实际上是说“当你准备好了,看看i”。因为该函数直到准备好触发时才检查i的值,所以它不会知道其值,直到你执行循环之后很久,这意味着i将是一个比你想要的数字高得多的数字!

作为上述的一个子要点,你需要立即调用那个函数。这称为立即调用的函数表达式。如果你不熟悉它们,它们肯定值得一看。它们的用途有点不寻常,但在正确的位置使用它们是一个强大的工具。

最后,因为你正在此处设置所有内容,所以要确保每个函数的超时间隔为一秒钟;现在,你正在说“从现在开始一秒钟内完成所有这些操作”,而你的意图是“从现在开始,每隔一秒钟完成所有这些操作”。这个修复相对容易;你只需要将你的超时乘以i,这样你就可以设置第一个在1秒后启动,第二个在2秒后启动,依此类推。

所有这些组合起来,给你的代码看起来像这样:

loopThroughSplittedText: function(splittedText) {

  for (var i = 0; i < splittedText.length; i++) {
    setTimeout(
      (function(locationInString) {
        return function() {
          console.log(splittedText[locationInString]);
        };
      }(i)),
      (1000*i)
    );
  }

},

关于哪种解决方案更好,我可能会推荐使用递归函数。递归版本将仅创建一个函数,为您传递的每个字符串调用自身,而for循环版本将为字符串中的每个字符创建一个函数,这可能会非常快速地失控。在JavaScript中,函数创建(以及对象创建)在处理较大项目时可能会变得昂贵,因此通常最好倾向于避免在可能的情况下创建大量函数的解决方案。
但是,出于解释的目的,我不想让您没有for循环版本的知识;在其他地方,这些知识可能也会派上用场。 :)

6
一次递归函数调用即可完成任务:
var a = [
    1,2,3,4,5,6,7,8,9,10
    ];

function log(i){
    console.log(a[i]);
    if (i<a.length){
       setTimeout(function(){
           i++;
           log(i);
       },1000);
    }
}

log(0);

http://jsfiddle.net/Curt/rjve4whe/1/


@BeyelerStudios 很好的发现!我没有注意到这一点,因为我的数据基本上与索引相同。 - Curtis
为防止溢出,应该写成:if (i<a.length-1) {...} - IT goldman
根据个人喜好,可以通过删除“i ++;”并改为运行“log(++i);”来缩短代码。 - Felix Geenen

4
在我的例子中,它将向您展示如何连续循环数组,直到停止。这只是为了让您了解如何延迟。它还会向您显示值实际上何时被显示。
我认为您实际上可以从此计时器创建一个不错的实用程序,并将其用于多个目的;使用此实用程序可以避免重复大量代码。
JavaScript循环示例:

var body = document.body;
var splittedText = ["Hello", "World", "How", "Are", "You", "Today"];

loopThroughArray(splittedText, function (arrayElement, loopTime) {
    body.innerHTML += arrayElement+ ": " + loopTime+ "<br/>";
}, 1000);

function loopThroughArray(array, callback, interval) {
    var newLoopTimer = new LoopTimer(function (time) {
        var element = array.shift();
        callback(element, time - start);
        array.push(element);
    }, interval);

    var start = newLoopTimer.start();
};

// Timer 
function LoopTimer(render, interval) {
    var timeout;
    var lastTime;

    this.start = startLoop;
    this.stop = stopLoop;

    // Start Loop
    function startLoop() {
        timeout = setTimeout(createLoop, 0);
        lastTime = Date.now();
        return lastTime;
    }
    
    // Stop Loop
    function stopLoop() {
        clearTimeout(timeout);
        return lastTime;
    }
    
    // The actual loop
    function createLoop() {
        var thisTime = Date.now();
        var loopTime = thisTime - lastTime;
        var delay = Math.max(interval - loopTime, 0);
        timeout = setTimeout(createLoop, delay);
        lastTime = thisTime + delay;
        render(thisTime);
    }
}


在你进行修改之前,我可以直接运行代码片段并显示结果。能否请你将它改回去,因为我正在按照你的答案进行工作?编辑:你已经改回去了。 - LoveAndHappiness
@LoveAndHappiness 对不起,我只是在做一些改进。让它可以在不同的地方使用计时器,而不必在不同的地方使用相同的代码。 - YaBCK
看起来有点过度,但我在这里看到它起作用了。如果在实现中遇到问题,我可能会再联系你,但非常感谢你的帮助。 - LoveAndHappiness
2
@ChrisBeckett 抱歉挖掘旧话题,但我该如何在函数外部停止loopThroughArray()?例如,通过一个按钮? - Rhecil Codes

3

好的,由于这不是完全重复的内容,您需要增加循环中的延迟时间,并且还需要解决循环中闭包变量的问题。

(参考:JavaScript 循环中的闭包变量问题

loopThroughSplittedText: function (splittedText) {
    splittedText.forEach(function (text, i) {
        setTimeout(function () {
            console.log(text);
        }, i * 1000)
    })
}

var obj = {
  loopThroughSplittedText: function(splittedText) {
    splittedText.forEach(function(text, i) {
      setTimeout(function() {
        document.getElementById('x').innerHTML += text
      }, i * 1000)
    })
  }
}

obj.loopThroughSplittedText('abcde'.split(''))
<div id="x"></div>


2
你的代码存在一个问题,就是所有回调函数都共用了变量i。因此,第一个回调函数被告知“输出在索引i处的条目”,但是当它开始执行时,初始循环已经结束,因此i现在在文本末尾。
实现你所需要的功能的一种方法是不使用for循环,而是编写一个函数,该函数(1)打印一个字符,(2)更新计数器/位置,(3)如果需要,则安排下一个字符:
loopThroughSplitText: function (text) {
    var i = 0;
    function printEntry() {
        console.log(text[i]);
        i++; // Increment the position
        if (i < text.length) { // If there are more chars, schedule another
            setTimeout(printEntry, 1000);
        }
    }
    printEntry(); // Print the first entry/char
}

2

使用闭包的解决方案 https://jsfiddle.net/x3azn/pan2oc9y/4/

function loopThroughSplittedText(splittedText) {
    var splittedText = ["Hello", "World", "How", "Are", "You", "Today"];
    for (var i = 0; i < splittedText.length; i++) {
        // for each iteration console.log a word
        // and make a pause after it
    (function(_i) {
        setTimeout(function() {
        window.document.getElementById('text').innerHTML = splittedText[_i];
        console.log(splittedText[_i]);
      }, 1000)
    }(i));

    }
}

loopThroughSplittedText()

1

针对这个问题,我们提供了另一种解决方案,即利用setTimeout的第三个参数,该参数仅在较新的浏览器中支持

(function (splittedText) {
   for (var i = 0; i < splittedText.length; i++) {
      setTimeout(
         function(val) { console.log(val); },
         i * 1000,
         splittedText[i]
      );
   }
})(["Hello", "world", "!"]);

API文档可以在这里查看(注意可选参数)。


你的代码片段不起作用,顺便说一下。至少 Stackoverflow 没有展示它的工作方式。(而且我使用的是最新版本的浏览器)。 - LoveAndHappiness
该API在您的浏览器中可能不可用。我已经测试过了,在IE 11、最新的Firefox和最新的Chrome中都能正常工作。 - Jaanus Varus
对不起,你是正确的。它确实可以工作。非常感谢你。 - LoveAndHappiness

1

另一种解决方案,使用 setInterval:

var i = 0;
var intv = setInterval(function() {
    if (i >= splittedText.length) {
        clearInterval(intv);
    } else {
        console.log(splittedText[i]);
        ++i;
    }
}, 1000);

1
这里有几个问题:
  1. setTimeout 应该接受一个函数,而不是调用函数的结果。
  2. setTimeout 立即返回,因此循环中的所有操作都会在大约相同的时间开始,并且在执行前等待 1000 毫秒(尽管上面的注释意味着它们都在同一时刻执行)。
  3. 由于没有将循环控制变量包装在闭包中,i 的值对于每次迭代都将等于 splittedText.length
你需要做的是,在移动到下一次循环迭代之前等待 setTimeout 指令执行。
例如:
var splittedText = ["Hello", "World", "How", "Are", "You", "Today"];

function loopThroughSplittedText(splittedText) {
    displayValue(splittedText,0);
}

function displayValue(arr, i){
    if(i<arr.length){
        setTimeout(function(){
            document.getElementById('text').innerHTML = arr[i];
            console.log(arr[i])
            displayValue(arr,i+1);
        },1000)
    }
}

loopThroughSplittedText(splittedText)

实时示例:http://jsfiddle.net/d3whkjww/1/


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