为什么脚本在浏览器显示页面之前完成

时间:2018-09-28 15:26:13

标签: javascript

在下面的代码中,为什么console.log的循环在显示任何HTML元素之前结束?我已将JavaScript代码放在HTML文件的末尾。

<!DOCTYPE html>
<html lang="en">

<body>

    <p id="counter"> no clicks yet </p>
    <script>
        for (i = 0; i < 99999; ++i) {
            console.log(i);
        }
        console.log("ready to react to your clicks");
    </script>
</body>

</html>

更新:

基于一个答案,我尝试了此操作,但仅在完全执行控制台日志循环后,仍然显示HTML文档:

<html lang="en">

<body onload="onLoad()">
    <button onclick="clickHandler()">Click me</button>
    <p id="counter"> no clicks yet </p>
    <script>
        var counter = 0;
        function clickHandler() {
            counter++;
            document.getElementById("counter").innerHTML = "number of clicks:" + counter;
        }
        function onLoad() {
            for (i = 0; i < 99999; ++i) {
                console.log(i);
            }
            console.log("ready to react to your clicks");
        }

    </script>
</body>

</html>

4 个答案:

答案 0 :(得分:8)

您要问其他问题。您的标题要求:

  

为什么console.log在生成DOM模型之前被执行?

我所理解的“在生成DOM模型之前”是“在创建p元素之前”。 p元素是在您的script元素运行之前创建的,并且可以从中访问它。如果您在脚本中执行document.getElementById("counter"),则将获得段落。

然后您问:

  

为什么console.log循环在显示任何HTML元素之前完成?

这是一个不同的问题。这里的问题不是解析已停止(与Simeon Stoykov suggested相反)。的确,分析已停止,但没有解释为什么未显示该段落。浏览器可以在执行script时显示该段落。 (实际上,如果您在script元素中放置了一个断点,Chrome会在该断点被点击时立即显示该段落。)

正在发生的事情是浏览器正在应用优化。浏览器延迟了可能重排(计算页面上元素的位置和几何形状)并渲染的时间的元素。目的是减少花在回流和渲染上的总时间。假设您有一个脚本来更改段落的样式,而不是将数字转储到控制台的脚本,该脚本会改变段落的位置或大小。天真的浏览器可能会这样做:

  1. 立即重排并渲染p#counter
  2. 执行脚本,该脚本将更新样式,以使p#counter的旧位置和大小发生变化。
  3. 重排并渲染p#counter以反映更改。

诸如FF或Chrome之类的真实浏览器将跳过上面的第一步,仅重排和渲染p#counter 一次,而不是两次

在像您这样的琐碎示例中,优化不是很好,但是可以想象一个复杂的页面,其中包含一堆表和一个启动脚本,该脚本立即用数据,图像,链接等行填充表中。能够减少X回流渲染操作的数量,只有一个回流渲染会产生很大的差异。

在您给出的示例中,浏览器知道用户在执行脚本时无法对页面执行任何操作,因此在脚本执行完毕之前,浏览器无法呈现页面。

此时出现问题:

  

如果浏览器延迟了计算元素的位置和大小,那为什么我可以在脚本中执行document.getElementById("counter").getBoundingClientRect()之类的操作并获取坐标

浏览器可以尽可能延迟 。当JavaScript代码查询元素的位置或大小时,就不再可能延迟。浏览器必须在那里进行重排,然后给出答案。 (在上面的讨论中,我讨论了重排和渲染以简化一点。但是重排并不一定要紧随渲染之后。因此,渲染可能仍会延迟到脚本完成执行之后。)


一个偶然的机会是您的示例旨在表示一个较长的计算,从而阻止了渲染并阻止用户获得任何反馈,解决该问题的方法是将冗长的工作分解为多个块:工作,然后使用setTimeout安排下一个块,依此类推,直到完成整个工作。例如:

const p = document.getElementById("counter");
let i = 0;
const limit = 100;
const chunkSize = 10;
function doWork() {
  for (let thisChunk = 0; thisChunk < chunkSize && i < limit; thisChunk++) {
    console.log(i++);
    p.textContent = i;
  }
  if (i < limit) {
    setTimeout(doWork, 0);
  }
}
doWork();

在上面的示例中,执行了一个由10个数字组成的块,然后setTimeout调度下一个块,脚本将控制权返回给JavaScript事件循环,该循环执行所需的任务。这包括响应用户交互,从而将页面呈现给用户。

在某些情况下,将整个工作分流到WebWorker中是很有意义的。

答案 1 :(得分:1)

有一些HTML渲染引擎(例如Chrome中的Blink)和用于编译JavaScript的引擎(例如Chrome中的V8)。

渲染过程分为四个阶段:

  • 构建DOM树:解析HTML文档并转换已解析的元素 到DOM树中的实际DOM节点。

  • 构造CSSOM树:CSSOM是指CSS对象模型,浏览器在构造DOM树以根据CSS调整DOM树时遇到了链接标记。

  • 构造渲染树:HTML中的可视化指令与CSSOM树中的样式数据结合在一起,用于创建渲染树。

  • 渲染树的布局:创建渲染器并将其添加到树时,它没有位置和大小。计算这些值称为布局。

  • 绘制渲染树:在此阶段,将遍历渲染树,并调用渲染器的paint()方法以在屏幕上显示内容。

当引擎到达标签时,脚本将被解析并执行,文档解析将停止,直到解析脚本完成为止。如果DOM中发生更改,引擎将在阶段5之前的阶段4(渲染树的布局)中应用该脚本。

它等待console.log完成执行,然后在显示DOM之前显示DOM,而不仅仅是执行它。

请参阅:How JavaScript works: the rendering engine and tips to optimize its performance

答案 2 :(得分:0)

当浏览器在解析HTML时看到脚本标记时,它将停止解析并开​​始运行脚本。在执行脚本之前,解析器不会继续。这是默认行为。这就是为什么即使在看到呈现页面之前也要执行脚本的原因。如果要在页面加载和呈现后执行脚本,可以使用其他技术,例如:

ModuleNotFoundError: No module named 'google.appengine'

awk 'BEGIN{RS="-----+";i=1} {print $0>i".sql"}{i++}'
for i in *.sql ; do name=$(sed -n 's/.*sub : \(.*\)/\1/p' $i) ; if [[ ! -z "$name" ]] ; then mv "$i" "${name}.sql" ; fi ; done    

<script defer>

答案 3 :(得分:0)

在循环开始之前,它并不能完全帮助渲染,但是比将onload放在body标签中更好-在循环仍在运行时显示DOM。

function onLoad() {
    for (i = 0; i < 99999; ++i) {
        console.log(i);
    }
    console.log("ready to react to your clicks");
}
window.addEventListener('load', onLoad);

如果要使用户仅在onLoad函数完成后才能单击按钮,我将停用它,并在执行onLoad函数后将其激活。