JavaScript 具有 for 循环的顺序性质

Sequential-ish nature of JavaScript with a for-loop

既然 JavaScript 是连续的(不包​​括异步能力),那么为什么它不 "seem" 像这个简化的例子那样按顺序运行:

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');
};

最后,什么会导致 disabled 属性直到 for 循环执行后才生效?只需注释掉 remove 属性行即可在视觉上验证它。

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

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

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

这样做是因为如果 Javascript 的一部分正在对 DOM 进行多项更改,重新布局文档然后在每次更改发生时重新绘制效率非常低等到 Javascript 完成执行,然后立即重新绘制所有更改。

这是一种特定于浏览器的优化方案,因此每个浏览器都会就何时重绘给定更改做出自己的实施决定,并且有些事件可以 cause/force 重绘。据我所知,这不是 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 回调中的其余代码 - 从而允许浏览器有机会 "breathe" 并进行重绘,因为它认为您当前的 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);

};

The browser doesn't render changes to the DOM until the function returns. - @Barmar

根据@Barmar 的评论和大量关于该主题的额外阅读,我将包括一个参考我的示例的摘要:

  • JavaScript是单线程的,所以一次只能发生一个进程
  • 渲染(重绘和回流)是浏览器执行的 separate/visual 过程,因此它在 函数完成后 出现以避免潜在的繁重 CPU/GPU如果即时渲染会导致 performance/visual 问题的计算

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

In most browsers, rendering and JavaScript use single event queue. It means that while JavaScript is running, no rendering occurs.

换一种方式解释,我将使用我在问题中提到的 setTimeout "hack":

  1. 单击 "run" 按钮将我的功能置于浏览器完成的 stack/queue 事物中
  2. 浏览器看到"disabled"属性,然后在stack/queue任务中添加渲染进程。
  3. 如果我们改为将 setTimeout 添加到函数的重部分,setTimeout(按设计)将其从当前流中拉出并将其添加到函数的末尾stack/queue。这意味着初始代码行将 运行, then 禁用属性的呈现, then long-运行ning for 循环代码;全部按照队列中的堆栈顺序排列。

有关上述内容的其他资源和解释: