在下面的代码中,为什么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>
答案 0 :(得分:8)
您要问其他问题。您的标题要求:
为什么console.log在生成DOM模型之前被执行?
我所理解的“在生成DOM模型之前”是“在创建p
元素之前”。 p
元素是在您的script
元素运行之前创建的,并且可以从中访问它。如果您在脚本中执行document.getElementById("counter")
,则将获得段落。
然后您问:
为什么console.log循环在显示任何HTML元素之前完成?
这是一个不同的问题。这里的问题不是解析已停止(与Simeon Stoykov suggested相反)。的确,分析已停止,但没有解释为什么未显示该段落。浏览器可以在执行script
时显示该段落。 (实际上,如果您在script
元素中放置了一个断点,Chrome会在该断点被点击时立即显示该段落。)
正在发生的事情是浏览器正在应用优化。浏览器延迟了可能重排(计算页面上元素的位置和几何形状)并渲染的时间的元素。目的是减少花在回流和渲染上的总时间。假设您有一个脚本来更改段落的样式,而不是将数字转储到控制台的脚本,该脚本会改变段落的位置或大小。天真的浏览器可能会这样做:
p#counter
。p#counter
的旧位置和大小发生变化。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函数后将其激活。