【问题标题】:JavaScript Infinitely Looping slideshow with delays?JavaScript无限循环幻灯片延迟?
【发布时间】:2011-08-15 15:52:30
【问题描述】:

如何在 JavaScript 中创建无限循环?我正在尝试制作幻灯片,我正在使用它,但我无法让它循环播放。我什至不能让它循环两次。

我现在使用的代码是

window.onload = function start() {
    slide();
}
function slide() {
    var num = 0;
    for (num=0;num<=10;num++) {
        setTimeout("document.getElementById('container').style.marginLeft='-600px'",3000);
        setTimeout("document.getElementById('container').style.marginLeft='-1200px'",6000);
        setTimeout("document.getElementById('container').style.marginLeft='-1800px'",9000);
        setTimeout("document.getElementById('container').style.marginLeft='0px'",12000);
    }
}

如果没有 for 东西,它确实会通过一次。当我输入一个 for 时,它要么使 Firefox 锁定,要么只循环一次。我确信这是一件非常简单的事情,即使它必须循环 1,000,000 次或其他东西而不是无限,这对我来说也很好。

另外,我不想使用 jQuery 或其他人创建的东西。我正在学习 JavaScript,这部分是为了帮助我学习,部分是因为我正在尝试制作尽可能多的基于 HTML5 的系统。

编辑:我认为它冻结的原因是因为它一次执行所有代码,然后将其存储在缓存或其他东西中。我想要它做的是经历一次,然后再次从顶部开始,这就是我一直认为的循环在哪里。在“批处理”(命令提示符)脚本中,这可以通过“GOTO”命令来完成。我不知道 JS 中是否有等价物,但这确实是我的目标。

【问题讨论】:

  • 不使用jQuery与使用HTML5有什么关系?
  • jQuery 不会阻止你学习 Javascript。它可以帮助隐藏每个浏览器的 javascript 实现中的(远远太多)差异。相信我,即使有 jquery 的帮助,你仍然会用 Javascript 弄脏你的手。
  • 哦,大多数浏览器不允许您在 javascript 中运行无限循环。如果有人这样做,那么对人们进行 DoS 攻击就太容易了。经过一定时间/次数的操作后,他们会弹出警告对话框,提示脚本可能行为不端,并让用户中止脚本。

标签: javascript loops


【解决方案1】:

正确的方法是使用单个计时器。使用setInterval,你可以实现你想要的如下:

window.onload = function start() {
    slide();
}
function slide() {
    var num = 0, style = document.getElementById('container').style;
    window.setInterval(function () {
        // increase by num 1, reset to 0 at 4
        num = (num + 1) % 4;

        // -600 * 1 = -600, -600 * 2 = -1200, etc 
        style.marginLeft = (-600 * num) + "px"; 
    }, 3000); // repeat forever, polling every 3 seconds
}

【讨论】:

  • 这行得通,谢谢 :) 这实际上是我最初尝试做的,但不知道如何添加到 marginLeft。编辑:这个答案也正是我想要的,因为如果我想添加 20 张幻灯片,它只需要一次快速编辑。谢谢,太好了。
  • @Rev:很乐意提供帮助,这里有很多令人困惑的答案,所以我认为加入我的解决方案是个好主意:-)
  • @Andy 这看起来和我的很像,主要的例外是(我认为)你的写得更漂亮:)
  • @Andy 我不怪你,我自己一直认为我必须在提供的答案混乱中错过一个答案(就像我们的答案一样)。我看了好几遍,以为我错过了它,但意识到实际上,几乎每个答案都是错误的。我确实看到你选择了setInterval 路线,所以你的答案不同:)
  • 太棒了。我觉得有点想玩这个,所以加了一点:jsfiddle.net/entropo/fy3vQ
【解决方案2】:

你不想要while(true),那会锁定你的系统。

您想要的是一个为自身设置超时的超时,如下所示:

function start() {
  // your code here
  setTimeout(start, 3000);
}

// boot up the first call
start();

【讨论】:

    【解决方案3】:

    这是一个不错的、整洁的解决方案:(also see the live demo ->)

    window.onload = function start() {
        slide();
    }
    
    function slide() {
        var currMarg = 0,
            contStyle = document.getElementById('container').style;
        setInterval(function() {
            currMarg = currMarg == 1800 ? 0 : currMarg + 600;
            contStyle.marginLeft = '-' + currMarg + 'px';
        }, 3000);
    }
    

    既然你正在努力学习,请允许我解释一下这是如何工作的。

    首先我们声明两个变量:currMargcontStylecurrMarg 是一个整数,我们将使用它来跟踪/更新容器应具有的左边距。我们在实际更新函数之外声明它(在closure 中),以便它可以持续更新/访问而不会丢失其价值。 contStyle 只是一个方便的变量,它使我们能够访问容器样式,而无需在每个间隔上定位元素。

    接下来,我们将使用setInterval 建立一个应该每 3 秒调用一次的函数,直到我们告诉它停止(这是你的无限循环,没有冻结浏览器)。它的工作原理与setTimeout 完全相同,只是它会无限发生直到被取消,而不是只发生一次。

    我们将anonymous function 传递给setInterval,这将为我们完成工作。第一行是:

    currMarg = currMarg == 1800 ? 0 : currMarg + 600;
    

    这是ternary operator。如果currMarg等于1800,它将为currMarg分配0的值,否则它将currMarg增加600

    在第二行中,我们只需将我们选择的值分配给containers marginLeft,就完成了!

    注意:在演示中,我将负值更改为正值,因此效果可见。

    【讨论】:

    • 呸呸呸!怎么说我们用最好的烟草烟斗庆祝我们的健康,我的好小伙子?
    • 请查看this 并考虑修改。
    • 另外,随着 ES2015 对 JavaScript 的改进,现在可以在一行中编写一个简单的格式化函数:pastebin.com/b1nVqKZZ
    • 您知道,在实际阅读您的实现时,我注意到一些仍然需要解决的硬编码。见this addendum。我可能最终会写下我自己的答案。
    【解决方案4】:

    也许这就是你要找的东西。

    var pos = 0;
    window.onload = function start() {
        setTimeout(slide, 3000);
    }
    function slide() {
       pos -= 600;
       if (pos === -2400)
         pos = 0;
       document.getElementById('container').style.marginLeft= pos + "px";
       setTimeout(slide, 3000);
    }
    

    【讨论】:

    【解决方案5】:

    您连续十次调用setTimeout(),因此它们几乎同时到期。你真正想要的是这样的:

    window.onload = function start() {
        slide(10);
    }
    function slide(repeats) {
        if (repeats > 0) {
            document.getElementById('container').style.marginLeft='-600px';
            document.getElementById('container').style.marginLeft='-1200px';
            document.getElementById('container').style.marginLeft='-1800px';
            document.getElementById('container').style.marginLeft='0px';
            window.setTimeout(
              function(){
                slide(repeats - 1)
              },
              3000
            );
        }
    }
    

    这将调用slide(10),然后设置3秒超时调用slide(9),设置超时调用slide(8)等。当调用slide(0)时,不再将设置超时。

    【讨论】:

    • 这违背了滑动照片展示的意义——在将容器移回原点之前,您不会将容器放置在原地任何时间!
    【解决方案6】:

    您可以通过递归轻松地无限循环。

    function it_keeps_going_and_going_and_going() {
      it_keeps_going_and_going_and_going();
    }
    
    it_keeps_going_and_going_and_going()
    

    【讨论】:

    • 这不是无限的,而是会在 stackoverflow 后崩溃。
    【解决方案7】:

    关键不是一次安排所有照片,而是每次显示一张照片时安排下一张照片。

    var current = 0;
    var num_slides = 10;
    function slide() {
        // here display the current slide, then:
    
        current = (current + 1) % num_slides;
        setTimeout(slide, 3000);
    }
    

    另一种方法是使用setInterval,它将功能设置为定期重复(而不是setTimeout,它只安排下一次出现。

    【讨论】:

      【解决方案8】:

      Ender's answer 上进行扩展,让我们通过 ES2015 的改进来探索我们的选项。


      首先,提问者代码中的问题是setTimeout 是异步的,而循环 是同步的。所以逻辑上的缺陷是他们从一个同步循环中编写了对异步函数的多个调用,期望它们同步执行。

      function slide() {
          var num = 0;
          for (num=0;num<=10;num++) {
              setTimeout("document.getElementById('container').style.marginLeft='-600px'",3000);
              setTimeout("document.getElementById('container').style.marginLeft='-1200px'",6000);
              setTimeout("document.getElementById('container').style.marginLeft='-1800px'",9000);
              setTimeout("document.getElementById('container').style.marginLeft='0px'",12000);
          }
      }
      

      然而,现实中发生的事情是……

      • 循环“同时”创建 44 个异步超时,设置为在未来 3、6、9 和 12 秒执行。 Asker 预计这 44 个调用会一个接一个地执行,但实际上它们都是同时执行的。
      • 循环结束 3 秒后,container 的 marginLeft 设置为 "-600px" 11 次。
      • 3 秒后,marginLeft 设置为"-1200px" 11 次。
      • 3 秒后,"-1800px",11 次。

      等等。

      您可以通过将其更改为:

      function setMargin(margin){
          return function(){
              document.querySelector("#container").style.marginLeft = margin;
          };
      }
      
      function slide() {
          for (let num = 0; num <= 10; ++num) {
              setTimeout(setMargin("-600px"), + (3000 * (num + 1)));
              setTimeout(setMargin("-1200px"), + (6000 * (num + 1)));
              setTimeout(setMargin("-1800px"), + (9000 * (num + 1)));
              setTimeout(setMargin("0px"), + (12000 * (num + 1)));
          }
      }
      

      但这只是一个懒惰的解决方案,并没有解决此实现的其他问题。这里有很多硬编码和一般的草率,应该修复。

      十年经验的教训

      正如本答案顶部所述,Ender 已经提出了一个解决方案,但我想补充一点,以考虑 ECMAScript 规范中的良好实践和现代创新。

      function format(str, ...args){
          return str.split(/(%)/).map(part => (part == "%") ? (args.shift()) : (part)).join("");
      }
      
      function slideLoop(margin, selector){
          const multiplier = -600;
          let contStyle = document.querySelector(selector).style;
      
          return function(){
              margin = ++margin % 4;
              contStyle.marginLeft = format("%px", margin * multiplier);
          }
      }
      
      function slide() {    
          return setInterval(slideLoop(0, "#container"), 3000);
      }
      

      让我们来看看这对所有初学者来说是如何工作的(请注意,并非所有这些都与问题直接相关):

      格式

      function format
      

      any 语言中具有类似 printf 的字符串格式化功能非常有用。我不明白为什么 JavaScript 似乎没有。

      format(str, ...args)
      

      ... 是 ES6 中添加的一个时髦的特性,可以让你做很多事情。我相信它被称为传播运算符。语法:...identifier...array。在函数头中,您可以使用它来指定变量参数,它将获取位于所述变量参数位置及其之后的每个参数,并将它们填充到一个数组中。您也可以使用数组调用函数,如下所示:args = [1, 2, 3]; i_take_3_args(...args),或者您可以获取类似数组的对象并将其转换为数组:...document.querySelectorAll("div.someclass").forEach(...)。如果没有展开运算符,这将是不可能的,因为querySelectorAll 返回一个“元素列表”,它不是一个真正的数组。

      str.split(/(%)/)
      

      我不擅长解释正则表达式的工作原理。 JavaScript 有两种用于正则表达式的语法。有 OO 方式 (new RegExp("regex", "gi")) 和字面方式 (/insert regex here/gi)。我对 regex 深恶痛绝,因为它鼓励的简洁语法往往弊大于利(也因为它们极不便携),但在某些情况下 regex 很有帮助,比如这个。通常,如果您使用"%"/%/ 调用split,则生成的数组将从数组中排除“%”分隔符。但是对于这里使用的算法,我们需要将它们包括在内。 /(%)/ 是我尝试的第一件事,它奏效了。我猜是幸运的猜测。

      .map(...)
      

      map 是一个函数式成语。您使用 map 将函数应用于列表。语法:array.map(function)。功能:必须返回一个值并接受 1-2 个参数。第一个参数将用于保存数组中的每个值,而第二个参数将用于保存数组中的当前索引。示例:[1,2,3,4,5].map(x =&gt; x * x); // returns [1,4,9,16,25]。另请参阅:filter、find、reduce、forEach。

      part => ...
      

      这是函数的另一种形式。语法:argument-list =&gt; return-value,例如(x, y) =&gt; (y * width + x),相当于function(x, y){return (y * width + x);}

      (part == "%") ? (args.shift()) : (part)
      

      ?: 运算符对是一个 3 操作数运算符,称为三元条件运算符。语法:condition ? if-true : if-false,虽然大多数人称它为“三元”运算符,因为在它出现的每种语言中,它都是唯一的三元运算符,其他所有运算符都是二元(+、&&、|、=)或一元( ++, ..., &, *)。有趣的事实:一些语言(以及语言的供应商扩展,如 GNU C)实现了?: 运算符的双操作数版本,语法为value ?: fallback,相当于value ? value : fallback,如果@,将使用fallback 987654360@ 评估为假。他们称之为猫王接线员。

      我还应该提到expressionexpression-statement 之间的区别,因为我意识到这对所有程序员来说可能并不直观。 expression 代表一个值,可以分配给l-value。表达式可以填充在括号内,不会被视为语法错误。表达式本身可以是 l-value,尽管大多数语句是 r-values,因为唯一的左值表达式是由标识符或(例如在 C 中)由引用/指针形成的那些。函数可以返回左值,但不要指望它。表达式也可以由其他较小的表达式合成。 (1, 2, 3) 是由三个 r 值表达式由两个逗号运算符连接而成的表达式。表达式的值为 3。另一方面,expression-statements 是由单个表达式组成的语句。 ++somevar 是一个表达式,因为它可以用作赋值表达式语句newvar = ++somevar; 中的右值(例如,表达式newvar = ++somevar 的值就是分配给newvar 的值) . ++somevar; 也是一个表达式语句。

      如果三元运算符让您感到困惑,请将我刚才所说的应用于三元运算符:expression ? expression : expression。三元运算符可以形成一个表达式或一个表达式语句,所以这两件事:

      smallest = (a < b) ? (a) : (b);
      (valueA < valueB) ? (backup_database()) : (nuke_atlantic_ocean());
      

      是运算符的有效用法。不过,请不要做后者。这就是if 的用途。有这种事情的情况,例如C 预处理器宏,但我们在这里讨论的是 JavaScript。

      args.shift()
      

      Array.prototype.shift。它是pop 的镜像版本,表面上继承自shell 语言,您可以在其中调用shift 以进入下一个参数。 shift 从数组中“弹出”第一个参数并返回它,在这个过程中改变数组。倒数是unshift。完整列表:

      array.shift()
          [1,2,3] -> [2,3], returns 1
      array.unshift(new-element)
          [element, ...] -> [new-element, element, ...]
      array.pop()
          [1,2,3] -> [1,2], returns 3
      array.push(new-element)
          [..., element] -> [..., element, new-element]
      

      参见:切片、拼接

      .join("")
      

      Array.prototype.join(string)。此函数将数组转换为字符串。示例:[1,2,3].join(", ") -&gt; "1, 2, 3"

      幻灯片

      return setInterval(slideLoop(0, "#container"), 3000);
      

      首先,我们返回setInterval 的返回值,以便稍后在调用clearInterval 时使用它。这很重要,因为 JavaScript 不会自己清理它。我强烈建议不要使用setTimeout 进行循环。这不是setTimeout 的设计目的,通过这样做,您将恢复到 GOTO。阅读 Dijkstra 1968 年的论文,Go To Statement Considered Harmful,了解为什么 GOTO 循环是不好的做法。

      其次,你会注意到我做了一些不同的事情。重复间隔是显而易见的。这将永远运行,直到间隔被清除,并且延迟 3000 毫秒。 callback 的值是另一个函数的返回值,我已经提供了参数0"#container"。这会创建一个闭包,您很快就会明白它是如何工作的。

      幻灯片循环

      function slideLoop(margin, selector)
      

      我们将边距 (0) 和选择器 ("#container") 作为参数。边距是初始边距值,选择器是用于查找我们正在修改的元素的 CSS 选择器。很简单。

      const multiplier = -600;
      let contStyle = document.querySelector(selector).style;
      

      我已经上移了一些硬编码元素。由于边距是 -600 的倍数,因此我们有一个带有该基值的明确标记的常数乘数。

      我还通过 CSS 选择器创建了对元素的 style 属性的引用。因为style 是一个对象,所以这样做是安全的,因为它将被视为引用而不是副本(阅读通过共享 来理解这些语义)。

      return function(){
          margin = ++margin % 4;
          contStyle.marginLeft = format("%px", margin * multiplier);
      }
      

      现在我们已经定义了范围,我们返回一个使用所述范围的函数。这称为闭包。你也应该阅读这些。从长远来看,了解 JavaScript 公认的奇怪的作用域规则将使该语言的痛苦减轻很多。

      margin = ++margin % 4;
      contStyle.marginLeft = format("%px", margin * multiplier);
      

      在这里,我们只需将边距和模数增加 4。这将产生的值序列是 1-&gt;2-&gt;3-&gt;0-&gt;1-&gt;...,它完全模仿了问题中的行为,没有任何复杂或硬编码的逻辑。

      之后,我们使用前面定义的format 函数轻松设置容器的marginLeft CSS 属性。它设置为当前边距值乘以乘数,您还记得它设置为 -600。 -600 -&gt; -1200 -&gt; -1800 -&gt; 0 -&gt; -600 -&gt; ...


      我的版本和 Ender 的版本之间存在一些重要差异,我在 a comment 的回答中提到了这一点。我现在要复习一下推理:

      使用document.querySelector(css_selector) 而不是document.getElementById(id)

      querySelector 是在 ES6 中添加的,如果我没记错的话。 querySelector(返回第一个找到的元素)和 querySelectorAll(返回所有找到的元素的列表)是 all DOM 元素原型链的一部分(不仅仅是document),并采用 CSS 选择器,所以除了通过 ID 查找元素之外,还有其他方法可以查找元素。您可以按 ID(#idname)、类(.classname)、关系(div.container div div spanp:nth-child(even))和属性(div[name]a[href=https://google.com])等进行搜索。

      始终跟踪 setInterval(fn, interval) 的返回值,以便以后可以使用 clearInterval(interval_id) 关闭它

      让区间永远运行并不是一个好的设计。编写一个通过setTimeout 调用自身的函数也不是好的设计。这与 GOTO 循环没有什么不同。 setInterval 的返回值应该被存储并用于在不再需要时清除间隔。将其视为一种内存管理形式。

      为了可读性和可维护性,将区间的回调放入自己的正式函数中

      这样的构造

      setInterval(function(){
          ...
      }, 1000);
      

      很容易变得笨重,尤其是当您存储 setInterval 的返回值时。我强烈建议将函数放在调用之外并给它一个名称,以便它清晰且自我记录。这也使得调用返回匿名函数的函数成为可能,以防您使用闭包(一种特殊类型的对象,包含围绕函数的本地状态)。

      Array.prototype.forEach 很好。

      如果状态与回调保持一致,则回调应从另一个函数(例如slideLoop)返回以形成闭包

      您不想像 Ender 那样将状态和回调混为一谈。这很容易出现混乱,并且可能变得难以维护。状态应该与匿名函数来自同一个函数,以便清楚地将其与世界其他部分区分开来。 slideLoop 的更好名称可能是 makeSlideLoop,只是为了更清楚。

      使用适当的空格。做不同事情的逻辑块应该用一个空行分隔

      这个:

      print(some_string);
      
      if(foo && bar)
          baz();
      
      while((some_number = some_fn()) !== SOME_SENTINEL && ++counter < limit)
          ;
      
      quux();
      

      比这更容易阅读:

      print(some_string);
      if(foo&&bar)baz();
      while((some_number=some_fn())!==SOME_SENTINEL&&++counter<limit);
      quux();
      

      很多初学者都这样做。包括 2009 年 14 岁的我,直到 2013 年我才改掉这个坏习惯。别再试图把你的代码压得这么小了。

      避免"string" + value + "string" + ...。做个格式化函数或者使用String.prototype.replace(string/regex, new_string)

      同样,这是一个可读性问题。这个:

      format("Hello %! You've visited % times today. Your score is %/% (%%).",
          name, visits, score, maxScore, score/maxScore * 100, "%"
      );
      

      比这个可怕的怪物更容易阅读:

      "Hello " + name + "! You've visited " + visits + "% times today. " + 
      "Your score is " + score + "/" + maxScore + " (" + (score/maxScore * 100) +
      "%).",
      

      编辑:我很高兴地指出我在上面的 sn-p 中犯了错误,我认为这很好地证明了这种字符串构建方法是多么容易出错。

      visits + "% times today"
                ^ whoops
      

      这是一个很好的演示,因为我犯这个错误的全部原因,而且我一直没有注意到它(没有),是因为代码非常难以阅读。

      始终用括号括住三元表达式的参数。它有助于提高可读性并防止错误。

      我从围绕 C 预处理器宏的最佳实践中借用了这条规则。但是我真的不需要解释这一点。自己看:

      let myValue = someValue < maxValue ? someValue * 2 : 0;
      let myValue = (someValue < maxValue) ? (someValue * 2) : (0);
      

      我不在乎您认为自己对语言语法的理解程度,后者总是比前者更容易阅读,而可读性是唯一必要的论据。您阅读的代码比编写的代码多数千倍。从长远来看,不要对未来的自己做个混蛋,这样你就可以在短期内称赞自己聪明。

      【讨论】:

        【解决方案9】:

        这里:

        window.onload = function start() {
            slide();
        }
        function slide() {
            var num = 0;
            for (num=0;num==10;) {
                setTimeout("document.getElementById('container').style.marginLeft='-600px'",3000);
                setTimeout("document.getElementById('container').style.marginLeft='-1200px'",6000);
                setTimeout("document.getElementById('container').style.marginLeft='-1800px'",9000);
                setTimeout("document.getElementById('container').style.marginLeft='0px'",12000);
            }
        }
        

        这使它保持循环正常!这就是它无法在此处运行的原因。

        【讨论】:

          【解决方案10】:

          试试这个:

          window.onload = function start() {
              slide();
          }
          function slide() {
               setInterval("document.getElementById('container').style.marginLeft='-600px'",3000);
               setInterval("document.getElementById('container').style.marginLeft='-1200px'",6000);
               setInterval("document.getElementById('container').style.marginLeft='-1800px'",9000);
               setInterval("document.getElementById('container').style.marginLeft='0px'",12000);
          }
          

          setInterval 基本上是一个“无限循环”,它不会让浏览器黑屏。它等待所需的时间,然后再次进行

          【讨论】:

          • 这会导致区间回调相互重叠,造成不良影响。
          • @andy 这比许多 setTimeouts 重叠 10X 更好
          • @Neal:原始的 setTimeouts 不重叠 - 它们在 3000、6000、9000 等处按顺序排列。使用您的方法,第一个计时器将在 3000...6000...9000 处触发。 .. 当它应该在 3000... 15000... 27000... 等时触发
          • @Andy,你的论点毫无意义
          • @Neal:如果您查看您的解决方案并将其与 OP 的方法进行比较,这是非常有意义的。 Oded 在他的答案中编辑了相同的方法这一事实并没有减少它的失败。
          【解决方案11】:

          您可以使用如下所示的 requestAnimationFrame() 函数,

          function unlimited () {
              requestAnimationFrame(unlimited);
              console.log("arian")
          }
          
          unlimited();
          

          【讨论】:

            猜你喜欢
            • 2022-11-08
            • 2012-09-19
            • 1970-01-01
            • 1970-01-01
            • 2011-12-30
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2012-07-13
            相关资源
            最近更新 更多