【问题标题】:Using setTimeout to update progress bar when looping over multiple variables循环多个变量时使用 setTimeout 更新进度条
【发布时间】:2011-10-07 17:00:22
【问题描述】:

假设您要循环遍历 3 个数组,长度分别为 x、y 和 z,并且对于每个循环,您想要更新一个进度条。例如:

function run() {
    x = 100;
    y = 100;
    z = 10;
    count = 0;
    for (i=0; i<x; i++) {
        //some code
        for (j=0; j<y; j++) {
            // some code
            for (k=0; k<z; k++) {
                //some code
                $("#progressbar").reportprogress(100*++count/(x*y*z));
            }
        }
    }
}

但是,在此示例中,进度条在函数完成之前不会更新。因此,我相信我需要在函数运行时使用 setTimeout 来更新进度条,尽管当你嵌套了 for 循环时我不知道该怎么做。

我需要将每个循环分解成自己的函数,还是可以将它们保留为嵌套的 for 循环?

我创建了一个 jsfiddle 页面以防你想运行当前函数:http://jsfiddle.net/jrenfree/6V4Xp/

谢谢!

【问题讨论】:

    标签: javascript jquery progress-bar settimeout


    【解决方案1】:

    TL;DR:使用 CPS:http://jsfiddle.net/christophercurrie/DHqeR/

    accepted answer(截至 2012 年 6 月 26 日)中的代码的问题在于它创建了一个超时事件队列,这些事件在三重循环退出之前不会触发。您实际上并没有实时看到进度条更新,而是看到了关于在内部闭包中捕获变量值时变量值的延迟报告。

    我希望您的“递归”解决方案看起来有点像使用continuation-passing style 来确保您的循环在您通过 setTimeout 获得控制权之前不会继续。您可能不知道您使用的是 CPS,但如果您使用 setTimeout 来实现循环,您可能已经非常接近它了。

    我已经详细说明了这种方法以供将来参考,因为了解它很有用,并且生成的演示比所提供的演示执行得更好。使用三重嵌套循环,它看起来有点复杂,因此对于您的用例来说可能有点过分,但在其他应用程序中可能很有用。

    (function($){
        function run() {
            var x = 100,
                y = 100,
                z = 10,
                count = 0;
    
            /*
            This helper function implements a for loop using CPS. 'c' is
            the continuation that the loop runs after completion. Each
            'body' function must take a continuation parameter that it
            runs after doing its work; failure to run the continuation
            will prevent the loop from completing.
            */
            function foreach(init, max, body, c) {
                doLoop(init);
                function doLoop(i) {
                    if (i < max) {
                        body(function(){doLoop(i+1);});
                    }
                    else {
                        c();
                    }
                }
            }
    
            /*
            Note that each loop body has is own continuation parameter (named 'cx',
            'cy', and 'cz', for clarity). Each loop passes the continuation of the
            outer loop as the termination continuation for the inner loop.
            */
            foreach(0, x, function(cx) {
                foreach(0, y, function(cy) {
                    foreach(0, z, function(cz) {
                        count += 1;
                        $('#progressbar').reportprogress((100*(count))/(x*y*z));
                        if (count * 100 % (x*y*z) === 0) {
                            /*
                            This is where the magic happens. It yields
                            control to the javascript event loop, which calls
                            the "next step of the foreach" continuation after
                            allowing UI updates. This is only done every 100
                            iterations because setTimeout can actually take a lot
                            longer than the specified 1 ms. Tune the iterations
                            for your specific use case.                   
                            */
                            setTimeout(cz, 1);
                        } else {
                            cz();
                        }
                    }, cy);
                }, cx);
            }, function () {});    
        }
    
        $('#start').click(run);
    })(jQuery);
    

    你可以在jsFiddle看到这个版本更新相当顺利。

    【讨论】:

    • 这工作正常,但为什么这么慢?对于 x = 10000, y = 1, z = 1(所以 10000 次迭代),运行大约需要 50 秒。有什么办法可以加快速度?
    • 当然,你可以不那么频繁地屈服;即使对于 1 毫秒的 setTimeout 请求,我也看到了高达 150 毫秒的延迟,这加起来超过 10000 次迭代。只需决定您的 UI 实际需要更新的频率。在这种情况下,您可能只关心百分比变化时的更新,因此最内层循环的最后一行可以替换为:if (count * 100 % (x*y*z) === 0) { setTimeout(cz, 1); } else cz();
    • 我已经更新了上面评论中指出的 jsFiddle 示例,并且在 10000 次迭代中性能要好得多。
    【解决方案2】:

    如果您想使用 setTimeout,您可以将 x、y、z 和计数变量捕获到闭包中:

    function run() {
        var x = 100,
            y = 100,
            z = 10,
            count = 0;
        for (var i=0; i<x; i++) {
            for (var j=0; j<y; j++) {
                for (var k=0; k<z; k++) {
                    (function(x, y, z, count) {
                        window.setTimeout(function() {
                            $('#progressbar').reportprogress((100*count)/(x*y*z));
                        }, 100);
                    })(x, y, z, ++count);
                }
            }
        }
    }
    

    Live demo.

    【讨论】:

    • 进度条更新好像不是很流畅。它会跳到几个百分点(2% 或 3%),然后停止,然后突然达到 100%。知道为什么要这样做吗?
    • 我确实需要使用 setTimeout,但我没有使用闭包,而是将主计算函数递归。我开始意识到 setTimeouts 不会暂停其余代码的处理,但您似乎可以使用递归函数强制 setTimeouts 运行。
    【解决方案3】:

    reportprogress 插件中的 jquery 函数可能使用了 setTimeout。例如,如果您使用 setTimeout 并使其在 0 毫秒后运行,这并不意味着它将立即运行。该脚本将在没有执行其他 javascript 时执行。

    在这里您可以看到我尝试在计数等于 0 时记录计数。如果我在 setTimeout 回调函数中执行此操作,则在所有周期后执行,您将获得 100000 无 0。这解释了为什么进度条仅显示100%。 js Fiddle link to this script

    function run() {
        x = 100;
        y = 100;
        z = 10;
        count = 0;
        for (i=0; i<x; i++) {
            //some code
            for (j=0; j<y; j++) {
                // some code
                for (k=0; k<z; k++) {
                    //some code
                    if(count===0) {
                         console.log('log emidiatelly ' + count);
                        setTimeout(function(){
                            console.log('log delayed ' + count);
                        },0);
                    }
                    count++;
                }
            }
        }
    }
    console.log('started');
    run();
    console.log('finished');
    

    在 setTimeout 回调函数中包装 for(i) 之后的所有内容使进度条工作。 js Fiddle link

    编辑: 刚刚检查了项目的样式设置代码实际上一直在执行。我认为首先执行javascript然后显示CSS更改可能是浏览器优先级。

    我写了另一个例子,我用 setInterval 函数替换了第一个 for 循环。像这样使用它有点错误,但也许你可以用这个 hack 解决这个问题。

    var i=0;
    var interval_i = setInterval(function (){
    
        for (j=0; j<y; j++) {
            for (k=0; k<z; k++) {
                $("#progressbar").reportprogress(100*++count/(x*y*z));
            }
        }
    
      i++;
      if((i<x)===false) {
        clearInterval(interval_i);
      }
    },0);
    

    JS Fiddle

    【讨论】:

    • 我已经尝试在我的代码中实现这一点,但有些东西不起作用。我猜这是由于其他处理线,其中许多访问索引变量 i、j 和 k
    【解决方案4】:

    我根据上次回复找到了解决方案,但将间隔时间更改为 1。此解决方案在主线程执行密集任务时显示加载程序。

    定义这个函数:

           loading = function( runme ) {
                        $('div.loader').show();
                        var interval = window.setInterval( function() { 
                              runme.call();  
                              $('div.loader').hide();
                              window.clearInterval(interval);
                        }, 1 );
                   };
    

    然后这样称呼它:

           loading( function() {
                    // This take long time...
                    data.sortColsByLabel(!data.cols.sort.asc);
                    data.paint(obj);
                  });
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-08
      • 2016-07-30
      • 1970-01-01
      • 1970-01-01
      • 2022-07-28
      • 1970-01-01
      相关资源
      最近更新 更多