【问题标题】:How to handle/schedule a massive event queue created by a javascript for loop如何处理/调度由 javascript for 循环创建的大量事件队列
【发布时间】:2012-06-20 00:01:16
【问题描述】:

我正在使用 jquery animate() 从 svg 标记中提取值,并创建动画。 (IE不支持SMIL,需要用脚本来完成。)

问题是:动画有数千个元素,每个元素都有自己的开始时间,因此我用来迭代每个元素的 for 循环会消耗几秒钟,在较慢的机器/浏览器上甚至更多.所以感觉就像在事件队列实际开始时,运行时已经运行了一段时间,并且已经过去了几秒钟,所以动画的最开始就提前了。动画从时间=零开始。 动画的开头有时会被剪掉。

不同的浏览器似乎对这个问题的处理方式不同,而且它似乎也取决于处理器的状态,所以我很难系统地弄清楚发生了什么。)这也是我第一次涉足动画和长得可怕的事情运行时,所以我确定我做错了什么:)

所以我的具体问题是如何避免这种行为。但更一般地说,事件队列的计时器是在运行时开始还是结束时开始?这究竟是如何工作的?

以下是大部分相关代码:

function runAnimation() {
  // create node list of paths
  var allPaths = document.getElementById('svgcontainer').getElementsByTagName('path');

  // define the animate function
  var doAnim = function(currentPath, dur, begin) {
    setTimeout(function(){
        $(currentPath).animate({'stroke-dashoffset': 0}, dur);
    }, begin);
  };

  // iterate through the nodelist
  for (var i=0; i<allPaths.length; i++) {

    var pathAnim = allPaths[i].firstChild;

    startTime = parseFloat(pathAnim.getAttribute('begin'));
    pathDuration = parseFloat(pathAnim.getAttribute('dur'));

// change times from seconds to milliseconds        
    startTime = startTime * 1000;
    pathDuration = pathDuration * 1000;

    doAnim(allPaths[i], pathDuration, startTime);
  }
}

【问题讨论】:

  • 如果您期望包含数千个对象的基于 javascript 的动画能够一直流畅,您可能会感到失望。
  • 你在说什么event queue timer?你在说setTimeout()吗?
  • @jfriend00:嗯,它们是连续的动画,一个接一个,有一些重叠。不是所有的同时运行。在大多数运行现代浏览器的机器上,它非常流畅。作为更一般的观点,请查看jstween.org 底部的演示。将其设置为 1000 个元素并播放它。在我的机器上它很光滑。可以做到的。

标签: jquery queue jquery-animate


【解决方案1】:

因为 javascript 是单线程的,所以如果你启动一个动画(它可能会在未来一段时间内执行 setTimeout()),然后你运行一堆其他的 javascript,这些 javascript 比第一次要长得多setTimeout(),然后 setTimeout() 将无法触发/运行,直到您的其他 javascript 执行完毕。当它最终运行时,它会意识到圣牛,它的方式,远远落后于计划,任何体面的补间算法都会尝试回到计划并跳过一堆动画的初始部分。这听起来像你所描述的所见。

解决此问题的唯一方法是避免在启动动画后运行代码的任何重要时间,因为正是运行的代码使动画落后于计划。如果您愿意仅针对此问题优化从开始到结束的动画过程,并且可以更改您自己的动画代码,则可以运行并初始化每个动画,以便预先计算所有初始状态,但没有实际动画的开始。然后,一旦运行所有代码来设置所有动画,您将运行一个非常快速的循环来启动它们全部运行。这将最小化第一个动画开始后的代码运行时间。

我现在真的不知道是什么花费了所有的初始化时间,也没有对时间的真正去向进行基准测试,但是您可以通过预先计算所有初始动画参数,将它们全部存储到一个数组,然后在您完成所有这些预先计算后启动所有动画:

想法 #1:

function runAnimation() {
  // create node list of paths
  var allPaths = document.getElementById('svgcontainer').getElementsByTagName('path');

  // define the animate function
  var doAnim = function(currentPath, dur, begin) {
    setTimeout(function(){
        $(currentPath).animate({'stroke-dashoffset': 0}, dur);
    }, begin);
  };

  var anims = [];

  // iterate through the nodelist
  for (var i=0, len = allPaths.length; i<len; i++) {

    var pathAnim = allPaths[i].firstChild;

    startTime = parseFloat(pathAnim.getAttribute('begin'));
    pathDuration = parseFloat(pathAnim.getAttribute('dur'));

// change times from seconds to milliseconds        
    startTime = startTime * 1000;
    pathDuration = pathDuration * 1000;

    // accumulate animation parameters, but don't start animation yet
    anims.push([allPaths[i], pathDuration, startTime]);
  }

  // now start all animations as fast as possible
  for (var i = 0, len = anims.length; i < len; i++) {
      doAnim.apply(this, anims[i]);
  }

}

老实说,这个代码更改看起来不会节省很多(这段代码中没有发生太多可能需要很多时间的事情),但如果循环的大小很大,它可能是一个有意义的改进。


想法 #2:

这是另一个想法。对动画进行排序,并在最后开始时间最快的动画上调用doAnim()。这使得 setTimeout() 不太可能在我们仍在初始化动画时触发。

function runAnimation() {
  // create node list of paths
  var allPaths = document.getElementById('svgcontainer').getElementsByTagName('path');

  // define the animate function
  var doAnim = function(currentPath, dur, begin) {
    setTimeout(function(){
        $(currentPath).animate({'stroke-dashoffset': 0}, dur);
    }, begin);
  };

  var anims = [];

  // iterate through the nodelist
  for (var i=0, len = allPaths.length; i<len; i++) {

    var pathAnim = allPaths[i].firstChild;

    startTime = parseFloat(pathAnim.getAttribute('begin'));
    pathDuration = parseFloat(pathAnim.getAttribute('dur'));

// change times from seconds to milliseconds        
    startTime = startTime * 1000;
    pathDuration = pathDuration * 1000;

    // accumulate animation parameters, but don't start animation yet
    anims.push([allPaths[i], pathDuration, startTime]);
  }

  // sort array so that smallest startTime values are last
  anims.sort(function(a, b) {
      return(b[2] - a[2]);
  });

  // Now start all animations as fast as possible
  // Because the array is sorted, it will start the longer setTimeout()
  // calls first and lessen the chance that the short ones will not get
  // get to fire when they want to
  for (var i = 0, len = anims.length; i < len; i++) {
      doAnim.apply(this, anims[i]);
  }
}

除此之外,任何其他修复都可能必须在 .animate() 代码本身中,因为它有自己的设置时间,所以如果太多对象都试图同时启动它们的动画,那不会保持平稳。


关于 SetTimeout() 如何与事件队列一起工作的注意事项:

以下是setTimeout() 与 javascript 事件队列一起工作的方式。系统计时器被安排在未来的某个时间。当达到该时间时,该计时器的事件将被放入 javascript 事件队列中。如果此时 javascript 引擎处于空闲状态,则立即执行相应的回调。如果 javascript 引擎此时正在执行其他操作,那么该计时器甚至只是停留在事件队列中。当当前正在执行的 javascript 线程完成其执行时,它会检查事件队列中是否还有更多事件。如果有一个事件在等待,它会从队列中拉出最旧的一个并开始执行它。该过程一直重复,直到一个执行的 javascript 线程完成并且队列中没有更多事件。虽然 javascript 事件一次只能执行一个,但新事件可以在计时器事件触发时实时添加到队列中(或发生鼠标点击或按键等...)。

如您所见,由于 javascript 是单线程的,如果同时有很多事情要做(比如要启动很多动画),那么实际上只有其中一件事情会准时启动其他的会延迟一些。

在您的特定代码中,如果您的代码试图同时启动一大堆动画(例如,所有动画都具有相同或非常接近的 startTime 值),那么您将启动第一个,然后是第二个将开始,第三个开始,等等......虽然所有其他人都开始了,但实际的动画还没有运行,因为他们实际显示动画的计时器被卡在所有试图获取的动画之后的队列中开始了。关键是要尽量减少任何动画运行后必须发生的工作量,并尝试分散工作量,以免一次性完成大量工作。

【讨论】:

  • 谢谢。不过,DoAnim 代码就在那里。代码的第五行 (!) for 循环正在迭代标记中的数千条路径。 doAnim 函数接受三个参数:路径、持续时间和开始时间。都在那里。
  • 然后,我的代码建议应该改进一些事情,因为它会尽可能接近 doAnim() 函数中的所有 setTimeout() 调用。如果这样做仍然存在问题,那么您将不得不做一些不同的事情,例如为每个 setTimeout() 添加一个短暂的延迟,所以在您完成所有设置之前,他们都不想触发。需要多少延迟取决于您计算机的速度。
  • 就基准测试而言,时间花在了从 DOM 中读取数据上。我看不出有什么办法。你明白了一般的想法,但是你说“代码正在运行”而不区分运行时和队列。 (我自己刚刚了解这一点,阅读了 Trevor Burnham 的新书。)例如:看看这个小提琴:jsfiddle.net/Xn7yP 你可能会在控制台中期待“1,2,3”,但你会得到“ 4,4,4”。这是因为 setTimeout 发生在事件队列中,与运行时完全不同,当运行时结束时,变量i 已递增到 4。
  • @Ben - 我添加了另一个代码选项。你试过我的第一个代码示例了吗?
  • @Ben - 我添加了一些关于 setTimeout() 如何与 javascript 事件队列一起工作的解释。
【解决方案2】:

不幸的是,关于动画和 JavaScript(jQuery) 的性能已经说过的话是非常正确的。在我多年来试图找到一种方法来做这些事情的过程中,我克服了一些障碍,但最终都是一样的:you have to give more than you'll get.

以下是您可以针对绩效问题采取的措施,以及为什么您付出的比得到的多。

内容、方式和原因

jQuery 使用 JavaScript 函数 setInterval 更新 DOM 以反映对象的变化。 (无论是位置、方面、不透明度——随便)。

该函数在 jQuery 中被重写为 step,并且 step 设置为每 13 毫秒触发一次。因为您要为如此多的对象设置动画——无论大小、形状、任何东西——它都会使浏览器超载。

那么我们能做些什么呢?

这里有两个选择,对你来说都不是一个优雅的解决方案。

您也可以更改触发动画的时间间隔。在执行此操作时,您必须考虑到您拥有的数千个元素,并决定哪种刷新率最适合您的性能与美学比率。

$.fx.interval

不过,您应该注意,有些浏览器对此的解释不同。还有几个插件旨在进一步优化它,并且有些成功。不过我不推荐,所以你必须用谷歌搜索才能找到。

附带说明,以上内容将更改所有动画的队列计时器。如果您只需要它处理特定的动画,您可以谷歌搜索以获得快速答案。这只是客观的信息。

或者,您可以在某些位置拍摄元素的快照,并将它们制作成更大的图像,您可以根据需要进行操作和交换。如果您使用爆炸或其他方式,这可能对您不起作用,但通常您可以使用此解决方案来获得预期的效果。

希望对你有所帮助

【讨论】:

  • 谢谢,但我认为您回答的问题略有不同。我不是在询问 jquery 的 setTimeout 间隔。我知道它是 13。我已经阅读了源代码。而且我不会同时为数千个元素设置动画。动画在几分钟内展开,在任何给定时间只有五个左右的元素在移动。一旦启动它就可以正常工作。这不是“为什么我的动画不流畅?”问题。我的问题是关于事件队列的性质。请重读?
【解决方案3】:

两点:

  • Javascript 是单线程的;
  • 每个 setTimeout 都会触发一个新线程。

将这两个事实加在一起,您可以看到您的for 循环保证在任何动画开始运行之前已经完成。

如果与startTime 延迟相比,for 循环需要很长时间才能完成,那么一旦 for 循环的线程完成,许多排队的动画就可以开始 - 所有精心计划的微定时都刚刚结束窗外。

希望这能让您深入了解您所描述的行为。

编辑:

基于 jfriend00 的想法,这里有一些东西可以(排除错误)实现与想法 #2 相同但避免昂贵排序的需要。

function runAnimation() {
    var allPaths = document.getElementById('svgcontainer').getElementsByTagName('path'),
        pathAnim, startTime, pathDuration, anims = [],
        i, j, arr;

    function doAnim(i, dur, begin) {
        setTimeout(function(){
            $(allPaths[i]).animate({'stroke-dashoffset': 0}, dur);
        }, begin);
    }

    // iterate through the nodelist
    for (i = 0, len = allPaths.length; i<len; i++) {
        pathAnim = allPaths[i].firstChild;
        startTime = pathAnim.getAttribute('begin');
        pathDuration = pathAnim.getAttribute('dur');
        // Accumulate animation parameters in such a way that 
        // we avoid the need for an expensive array sort later
        if(!anims[startTime]) {
            anims[startTime] = [];//Allow for multiple events per startTime
        }
        anims[startTime].push([i, pathDuration*1000, startTime*1000]);
    }

    // At this point we have a sparse array indexed by startTime and containing arrays of animation data.
    // To iterate over a sparse array efficiently, we have to do something slightly tricky
    // ref: http://hexmen.com/blog/2006/12/iterating-over-sparse-arrays/
    for (arr in anims) {
        if (String(arr >>> 0) == arr && arr >>> 0 != 0xffffffff) {
            for(j = 0, len = arr.length; j < len; j++) {
                doAnim.apply(this, arr[j]);
            }
        }
    }
}

不要试图理解稀疏迭代器中的&gt;&gt;&gt; 东西。我自己并不完全理解;我只知道它有效;-)。但是,我之前没有在大容量、时间紧迫的环境中使用它,所以看看它是否足够快会很有趣。

【讨论】:

  • 谢谢。我想我想知道的是,您的第二段和第三段实际上对我来说似乎是矛盾的。如果整个 for 循环必须在事件队列开始之前完全完成,为什么我会看到跳过?如果这是真的,那么计算 for 循环所需的处理时间长度应该是完全不相关的。浏览器计划动画,只有当一切准备就绪时,它才会启动它,启动事件队列。 for 循环可能需要几分钟(理论上)。
  • 在这种情况下,用户应该会遇到初始延迟,但一旦开始就不会跳过。在这个(我称之为理论#1)中,time=0 在运行时结束时开始,在结束脚本标记处,可以这么说。但是在您的第三段(我将其称为理论#2)中,处理时间绝对至关重要。在这个理论中,提示事件的时间实际上是在运行时开始的。
  • (在我的运行时间结束时,实际上有数千个提示事件。每隔一秒左右,就会启动另外五个动画。开始时间是错开的。)这真的切入了核心我在我的OP中问。事件队列的计时是在 for 循环执行期间的某个时刻开始,还是在循环结束时开始?这对我来说并不明显。我想看看参考。再次感谢。
  • 一旦您清除了“事件队列”的概念,一切都会变得清晰。这是不正确的。 SetTimeouts 安排一个函数在未来 x 毫秒触发,从 setTimout 语句执行的那一刻开始。因此,时钟独立地在每个延迟函数上滴答作响。没有共同的起点。在您冗长的for 循环中,那些持续时间较短的 setTimeouts 将在循环完成之前准备好触发,并且一旦循环(等)完成后将以非常短的顺序触发......因此您所说的“跳过”。
  • 谢谢。我得到它!我需要一些时间来了解稀疏迭代器,但这是一个非常有用的技巧。
猜你喜欢
  • 1970-01-01
  • 2018-03-20
  • 1970-01-01
  • 2018-09-18
  • 2021-04-03
  • 1970-01-01
  • 2017-10-12
  • 1970-01-01
  • 2011-09-09
相关资源
最近更新 更多