【问题标题】:Javascript - how to avoid blocking the browser while doing heavy work?Javascript - 如何在做繁重的工作时避免阻塞浏览器?
【发布时间】:2012-04-28 03:40:02
【问题描述】:

我的 JS 脚本中有这样一个函数:

function heavyWork(){
   for (i=0; i<300; i++){
        doSomethingHeavy(i);
   }
}

也许“doSomethingHeavy”本身没问题,但重复 300 次会导致浏览器窗口卡住一段不可忽略的时间。在 Chrome 中,这不是什么大问题,因为只有一个 Tab 会受到影响;但对于 Firefox 来说,这完全是一场灾难。

有什么方法可以告诉浏览器/JS“放轻松”,而不是阻止调用 doSomethingHeavy 之间的所有内容?

【问题讨论】:

  • 您可以尝试在调用 doSomethingHeavy 之间睡觉。此外,如果此函数处理与某些服务器的通信,AJAX 可能是正确的选择。
  • @EranZimmerman:我认为你不能在 JavaScript 中“睡觉”。你能做的最好的就是setTimeout 每次调用doSomethingHeavy

标签: javascript


【解决方案1】:

您可以将调用嵌套在 setTimeout 调用中:

for(...) {
    setTimeout(function(i) {
        return function() { doSomethingHeavy(i); }
    }(i), 0);
}

这会将对 doSomethingHeavy 的调用排队等待立即执行,但其他 JavaScript 操作可以插入它们之间。

更好的解决方案是让浏览器通过Web Workers 生成一个新的非阻塞进程,但这是特定于 HTML5 的。

编辑:

使用setTimeout(fn, 0) 实际上需要比零毫秒更长的时间——例如 Firefox,enforces a minimum 4-millisecond wait time。更好的方法可能是使用 setZeroTimeout,它更喜欢 postMessage 用于瞬时、可中断的函数调用,但使用 setTimeout 作为旧浏览器的后备.

【讨论】:

  • 应该是setTimeout((function(i){
  • 就我而言,这两种技术都很有用(适用于不同类型的繁重工作)。谢谢!
  • 干得好。我认为可能值得注意的是,在doSomethingHeavy期间浏览器会被冻结,所以如果有长时间的操作,仍然会出现挂起,但会是1/300的时间。跨度>
  • 为什么 setZeroTimeout 可能是更好的方法?有点不公平吧?这里 setTimeout 的目的与延迟无关,而是让其他代码有机会运行。
  • @TobbeBrolin setTimeout(fn, 0) 的问题是(是吗?现在已经 6 岁了)setTimeout最小等待时间为 4 毫秒。如果你需要运行fn 1000 次,那是额外浪费 4 秒时间的上限。通过等待“零”毫秒,您仍然可以为其他进程在迭代之间对接(您确实仍然使用setZeroTimeout 推迟任何排队等待的任务),但如果没有其他等待,您不会浪费 4 毫秒任务队列。
【解决方案2】:

你可以做很多事情:

  1. 优化循环 - 如果繁重的工作与 DOM 访问有关,请参阅answer
    • 如果函数正在处理某种原始数据,请使用类型化数组MSDNMDN
  2. 带有 setTimeout() 的方法称为 eteration。很有用。

  3. 对于非函数式编程语言,该函数似乎非常简单。 JavaScript 获得了回调 SO question 的优势。

  4. 一个新功能是网络工作者MDNMSDNwikipedia

  5. 最后一件事(也许)是结合所有的方法 - 传统的方法是函数只使用一个线程。如果您可以使用网络工作者,您可以将工作分配给几个人。这应该可以最大限度地减少完成任务所需的时间。

【讨论】:

    【解决方案3】:
    function doSomethingHeavy(param){
       if (param && param%100==0) 
         alert(param);
    }
    
    (function heavyWork(){
        for (var i=0; i<=300; i++){
           window.setTimeout(
               (function(i){ return function(){doSomethingHeavy(i)}; })(i)
           ,0);
        }
    }())
    

    【讨论】:

    • 以上代码无效。 setTimeout 不会神奇地保存周围的计算上下文,也不会将控制权交还给浏览器。这是一种调度机制。上面的解释是错误的。
    • @dyoo:我认为这不对,也许正确的方法是0,但我很确定它会在等待时跳转到堆栈中的下一个。
    • 没有。作为证据,请尝试:var test = function() { var f = function() {alert('hi!');}; setTimeout(f, 0); while(true) {} }; test()。在您的演示文稿中,这会说“嗨!”。请注意,它不会这样做。我试图指出仅添加setTimeout() 是不够的。调用setTimeout 不会将控制权交还给浏览器:它只是一种调度机制。
    • 我明白你在说什么,你是对的,它是关于调度(但浏览器控制也是如此)。问题是它必须发生在doSoemthingHeavy 之外,除非您创建自己的队列堆栈并从doSomethingHeavy 中检查它。自从我最初发布以来,似乎有很多答案,所以我给你一些时间看这个评论后就删除了。
    【解决方案4】:

    我们需要时不时地把控制权交给浏览器,以免垄断浏览器的注意力。

    释放控制权的一种方法是使用setTimeout,它安排在某个时间段调用“回调”。例如:

    var f1 = function() {
        document.body.appendChild(document.createTextNode("Hello"));
        setTimeout(f2, 1000);
    };
    
    var f2 = function() {
        document.body.appendChild(document.createTextNode("World"));
    };
    

    在此处调用f1 会将单词hello 添加到您的文档中,安排待处理的计算,然后将控制权释放给浏览器。最终,f2 将被调用。

    请注意,将setTimeout 不加选择地散布在整个程序中是不够的,就好像它是神奇的小精灵尘埃:您确实需要将其余的计算封装在回调中。通常情况下,setTimeout 将是函数中的最后一件事,其余的计算将被塞入回调中。

    对于您的特定情况,需要将代码仔细转换为以下内容:

    var heavyWork = function(i, onSuccess) {
       if (i < 300) {
           var restOfComputation = function() {
               return heavyWork(i+1, onSuccess);
           }
           return doSomethingHeavy(i, restOfComputation);          
       } else {
           onSuccess();
       }
    };
    
    var restOfComputation = function(i, callback) {
       // ... do some work, followed by:
       setTimeout(callback, 0);
    };
    

    这将在每个restOfComputation 上将控制权释放给浏览器。

    作为另一个具体示例,请参阅:How can I queue a series of sound HTML5 <audio> sound clips to play in sequence?

    高级 JavaScript 程序员需要知道如何进行这种程序转换,否则他们会遇到您遇到的问题。您会发现,如果您使用这种技术,您将不得不以一种特殊的风格编写程序,其中每个可以释放控制权的函数都接收一个回调函数。这种风格的技术术语是“延续传递风格”或“异步风格”。

    【讨论】:

      【解决方案5】:

      你需要使用 Web Workers

      https://developer.mozilla.org/En/Using_web_workers

      如果你在 google 上搜索一下,就会有很多关于 web workers 的链接

      【讨论】:

        【解决方案6】:

        您可以尝试将每个函数调用包装在 setTimeout 中,超时为 0。这会将调用推到堆栈底部,并且应该让浏览器在每个调用之间休息。

        function heavyWork(){
           for (i=0; i<300; i++){
                setTimeout(function(){
                    doSomethingHeavy(i);
                }, 0);
           }
        }
        

        编辑:我刚刚意识到这行不通。每次循环迭代的i 值都相同,您需要进行闭包。

        function heavyWork(){
           for (i=0; i<300; i++){
                setTimeout((function(x){
                    return function(){
                        doSomethingHeavy(x);
                    };
                })(i), 0);
           }
        }
        

        【讨论】:

        • 0 不再是可接受的延迟值,您至少需要使用 4
        • @Teemu:[需要引用] 编辑:找到它developer.mozilla.org/en/DOM/…
        • @Teemu:根据spec,这应该自动发生:If the currently running task is a task that was created by the setTimeout() method, and timeout is less than 4, then increase timeout to 4
        • 我刚刚为您找到了相同的 MDN 链接...它已经在这里了。最近我一直在“翻译”一些旧的 HTA 以使用 IE9 实用程序,并且我不得不将每个小于 4 的超时和间隔延迟更改为 4。如果低于,则不会调用任何调用。
        【解决方案7】:

        我看到了两种方式:

        a) 您可以使用 Html5 功能。那么你可以考虑使用工作线程。

        b) 您拆分此任务并将一条消息排队,该消息一次只执行一个调用,并且只要有事情要做就迭代。

        【讨论】:

          【解决方案8】:

          有人编写了一个特定的 backgroundtask javascript 库来完成如此繁重的工作。你可以在这里查看这个问题:

          Execute Background Task In Javascript

          我自己没用过,只是使用了也提到的线程用法。

          【讨论】:

            猜你喜欢
            • 2015-03-04
            • 2016-04-13
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-12-29
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多