JavaScript脚本的工作原理

课后整理 2020-12-20

下面我们简单了解一下JavaScript脚本的工作原理。

浏览器加载JavaScript脚本,主要通过<script>标签完成。正常的网页加载流程是这样的。

第1步,浏览器一边下载HTML网页,一边开始解析。

第2步,解析过程中,发现<script>标签。

第3步,暂停解析,网页渲染的控制权转交给JavaScript引擎。

第4步,如果<script>标签引用了外部脚本,就下载该脚本,否则就直接执行。

第5步,执行完毕,控制权交还渲染引擎,恢复往下解析HTML网页。

加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后,再继续渲染。原因是JavaScript可以修改DOM(如使用document.write方法),所以必须把控制权让给它,否则会导致复杂的线程竞赛问题。

如果外部脚本加载时间很长(如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。

为了避免这种情况,较好的做法是将<script>标签都放在页面底部,而不是头部。这样即使遇到脚本失去响应,网页主体的渲染也已经完成了,用户至少可以看到内容,而不是面对一张空白的页面。

如果某些脚本代码非常重要,一定要放在页面头部的话,最好直接将代码嵌入页面,而不是连接外部脚本文件,这样能缩短加载时间。

将脚本文件都放在网页尾部加载,还有一个好处。在DOM结构生成之前就调用DOM,JavaScript会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时DOM肯定已经生成了。

<head>
<script>
console.log(document.body.innerHTML);
</script>
</head>
<body>
</body>

上面代码执行时会报错,因为此时document.body元素还未生成。

一种解决方法是,设定DOMContentLoaded事件的回调函数。

<head>
<script>
document.addEventListener(
    'DOMContentLoaded',
    function (event) {
        console.log(document.body.innerHTML);
    }
);
</script>
</head>

另一种解决方法是,使用<script>标签的onload属性。当<script>标签指定的外部脚本文件下载和解析完成,会触发一个load事件,可以把所需执行的代码,放在这个事件的回调函数里面。

<script  src="jquery.min.js"  onload="console.log(document.body.innerHTML)">
</script>

但是,如果将脚本放在页面底部,就可以完全按照正常的方式写,上面两种方式都不需要。

<body>
<!-- 其他代码  -->
<script>
console.log(document.body.innerHTML);
</script>
</body>

如果有多个script标签,如下面这样。

<script  src="a.js"></script>
<script  src="b.js"></script>

浏览器会同时并行下载a.js和b.js,但是,执行时会保证先执行a.js,然后再执行b.js,即使后者先下载完成,也是如此。也就是说,脚本的执行顺序由它们在页面中的出现顺序决定,这是为了保证脚本之间的依赖关系不受到破坏。当然,加载这两个脚本都会产生“阻塞效应”,必须等到它们都加载完成,浏览器才会继续页面渲染。

Gecko和Webkit引擎在网页被阻塞后,会生成第二个线程解析文档,下载外部资源,但是不会修改DOM,网页还是处于阻塞状态。

解析和执行CSS,也会产生阻塞。Firefox会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。

此外,对于来自同一个域名的资源,如脚本文件、样式表文件、图片文件等,浏览器一般最多同时下载六个(IE11允许同时下载13个)。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度。