具有for循环

时间:2015-05-19 19:47:33

标签: javascript for-loop sequential

由于JavaScript是顺序的(不计算异步能力),那么为什么它不会"似乎"按照这个简化的例子来表现顺序:

HTML:

<input type="button" value="Run" onclick="run()"/>

JS:

var btn = document.querySelector('input');

var run = function() {
    console.clear();
    console.log('Running...');
    var then = Date.now();
    btn.setAttribute('disabled', 'disabled');

    // Button doesn't actually get disabled here!!????

    var result = 0.0;
    for (var i = 0; i < 1000000; i++) {
        result = i * Math.random();
    }

    /*
    *  This intentionally long-running worthless for-loop
    *  runs for 600ms on my computer (just to exaggerate this issue),
    *  meanwhile the button is still not disabled
    *  (it actually has the active state on it still
    *  from when I originally clicked it,
    *  technically allowing the user to add other instances
    *  of this function call to the single-threaded JavaScript stack).
    */

    btn.removeAttribute('disabled');

    /*
    *  The button is enabled now,
    *  but it wasn't disabled for 600ms (99.99%+) of the time!
    */

    console.log((Date.now() - then) + ' Milliseconds');
};

最后,在 for循环执行发生之后,会导致disabled属性生效的原因是什么?只需注释掉删除属性行即可在视觉上验证。

我应该注意,不需要延迟回调,承诺或任何异步;然而,我发现的唯一工作是在零延迟的setTimeout回调中包围for循环和剩余行,这将把它放在一个新的堆栈中...但是真的吗?setTimeout应该基本上逐行工作?

这里真正发生了什么,为什么在for循环运行之前setAttribute没有发生?

2 个答案:

答案 0 :(得分:1)

出于效率原因,浏览器不会立即布局并显示您在更改时立即对DOM所做的每一次更改。在许多情况下,DOM更新被收集到批处理中,然后在稍后的某个时间(例如,当前JS的完成时)完成更新。

这样做是因为如果一个Javascript对DOM进行了多次更改,那么重新布局文档然后重新发生每个更改的效率非常低效,并且要等到Javascript完成执行然后重新绘制时效率要高得多所有的变化一下子。

这是一个特定于浏览器的优化方案,因此每个浏览器都会自行决定何时重新绘制给定的更改,并且有一些事件可能导致/强制重绘。据我所知,这不是ECMAScript指定的行为,只是每个浏览器实现的性能优化。

在属性准确之前,有些DOM属性需要完成布局。通过Javascript访问这些属性(甚至只是阅读它们)将强制浏览器对任何挂起的DOM更改进行布局,并且通常也会导致重新绘制。其中一个属性是.offsetHeight,还有其他属性(尽管此类别中的所有属性都具有相同的效果)。

例如,您可以通过更改此内容来重新绘制:

btn.setAttribute('disabled', 'disabled');

到此:

btn.setAttribute('disabled', 'disabled');
// read the offsetHeight to force a relayout and hopefully a repaint
var x = btn.offsetHeight;

如果你想进一步阅读这篇文章,那么Google search for "force browser repaint"就会有很多关于这个主题的文章。

如果浏览器仍然无法重新绘制,则其他解决方法是隐藏,然后显示一些元素(这会导致布局变脏)或使用setTimeout(fn, 1);继续浏览你的代码在setTimeout回调中的其余部分 - 从而允许浏览器有机会“呼吸”#34;并重新绘制,因为它认为你当前的Javascript执行线程已经完成。

例如,您可以像这样实施setTimeout解决方法:

var btn = document.querySelector('input');

var run = function() {
    console.clear();
    console.log('Running...');
    var then = Date.now();
    btn.setAttribute('disabled', 'disabled');

    // allow a repaint here before the long-running task
    setTimeout(function() {

        var result = 0.0;
        for (var i = 0; i < 1000000; i++) {
            result = i * Math.random();
        }

        /*
        *  This intentionally long-running worthless for-loop
        *  runs for 600ms on my computer (just to exaggerate this issue),
        *  meanwhile the button is still not disabled
        *  (it actually has the active state on it still
        *  from when I originally clicked it,
        *  technically allowing the user to add other instances
        *  of this function call to the single-threaded JavaScript stack).
        */

        btn.removeAttribute('disabled');

        /*
        *  The button is enabled now,
        *  but it wasn't disabled for 600ms (99.99%+) of the time!
        */

        console.log((Date.now() - then) + ' Milliseconds');
    }, 0);

};

答案 1 :(得分:0)

  

在该函数之前,浏览器不会对DOM进行更改   回报。 - @Barmar

Per @ Barmar的评论和关于这个主题的大量补充阅读,我将包括一个参考我的例子的摘要:

  • JavaScript是单线程的,因此一次只能有一个进程
  • 渲染(重绘和重排)是浏览器执行的单独/可视化过程,因此在函数完成后来到,以避免可能导致性能/视觉问题的可能繁重的CPU / GPU计算如果在飞行中呈现

另一种方式是来自http://javascript.info/tutorial/events-and-timing-depth#javascript-execution-and-rendering

的引用
  

在大多数浏览器中,渲染和JavaScript使用单个事件队列。这意味着在JavaScript运行时,不会进行渲染。

为了解释另一种方式,我将使用我在问题中提到的 setTimeout “hack”:

  1. 单击“运行”按钮将我的函数放入事物的堆栈/队列中以供浏览器完成
  2. 看到“禁用”属性,浏览器会将渲染过程添加到任务的堆栈/队列中。
  3. 如果我们将 setTimeout 添加到函数的重要部分,则setTimeout(按设计)将其拉出当前流并将其添加到堆栈/队列的末尾。这意味着初始代码行将运行,然后呈现禁用属性,然后长期运行的for循环代码;所有按照排队的顺序排列。
  4. 有关上述内容的其他资源和解释: