8
JavaScript 是如何工作的系列——第一篇

前言

我们都知道在默认情况下,JavaScript 的下载和执行会阻塞 HTML 的解析,结果会导致从打开页面到显示出网页内容的过程耗时较长,用户体验不好。本篇文章主要介绍了 JavaScript 的下载和执行是如何阻塞 HTML 解析的 ,以及如何避免阻塞。

JavaScript 的下载和执行

我们以下面这段代码为例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Demo</title>
    <script src='./js/index1.js'></script>
    <script src='./js/index2.js'></script>
    <script src='./js/index3.js'></script>
</head>
<body>
    <div>Downloading Scripts to Execution</div>
</body>
</html>

index1.js 文件内容:

console.log('index1');

index2.js 文件内容:

console.log('index2');

index3.js 文件内容:

console.log('index3);

当我们在浏览器中打开上述 HTML 文件之后,浏览器就会开始解析 HTML 代码。当浏览器遇到 <head> 标签中的第一个 <script> 标签(index1.js)后,HTML 的解析会暂停。此时浏览器会发送一个 HTTP 请求去下载 index1.js 文件,当脚本下载完成,浏览器开始解析并解释执行下载的脚本( index1.js)。当 index1.js 执行完毕,浏览器继续解析 HTML 代码。当遇到第二个 <script> 标签(index2.js)后,处理过程同上,后面依次类推。

Image  6

由此可见,脚本的数量与消耗的时间是成正比的。在<head> 标签中引入的脚本越多,用户等待的时间越长。

避免阻塞方式

那么有什么办法可以解决上述问题呢?

最常见的方式就是将 <script> 标签放到 HTML 文档最后,</body>标签之前。这会提高网页加载速度,因为 HTML 加载不再受制于脚本加载,当 HTML 内容解析完毕后,才会开始加载脚本。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Demo</title>
</head>
<body>
    <div>Downloading Scripts to Execution</div>
    <script src='./js/index1.js'></script>
    <script src='./js/index2.js'></script>
    <script src='./js/index3.js'></script>
</body>
</html>

当然还有两种方式可以解决上述问题:(只适用于外部脚本)

  • defer 属性
  • async 属性(HTML5)

defer

使用 defer 属性后:

  • JavaScript 脚本下载在新的线程中进行,不会阻塞 HTML 解析;
  • 脚本下载完成后,不立即执行,在 HTML 解析完成后按照顺序执行。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Demo</title>
    <script defer src='./js/index1.js'></script>
    <script defer src='./js/index2.js'></script>
    <script src='./js/index3.js'></script>
</head>
<body>
    <div>Downloading Scripts to Execution</div>
</body>
</html>

defer

async

使用 async 属性后:

  • JavaScript 脚本下载在新的线程中进行,不会阻塞 HTML 解析;(同 defer)
  • 脚本下载完成后,立即解析,此时会阻塞 HTML 的解析。解析完毕后,在另一个线程中解释执行,此时不会阻塞 HTML 的解析。(多个脚本的执行顺序无法预测)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Demo</title>
    <script async src='./js/index1.js'></script>
    <script async src='./js/index2.js'></script>
    <script src='./js/index3.js'></script>
</head>
<body>
    <div>Downloading Scripts to Execution</div>
</body>
</html>

async

从上图中可以看到,JavaScript 脚本的执行顺序是:index1.js -> index3.js -> index2.js,这可能与你预想的执行顺序不同。这是因为使用 async 属性的脚本,其执行顺序与文件大小、下载速度、解析速度等息息相关。

总结

comparison

本文示例代码已上传到 github,感兴趣的读者可以自行下载。本地运行后,观察使用不同的方式引入 JavaScript 脚本后,页面的加载速度。

下一篇

本篇文章主要介绍了浏览器是何时开始下载和执行 JavaScript 的 ,以及阻塞 HTML 解析问题。下篇文章将深入 JS 引擎,了解 JavaScript 代码执行的具体流程。

传送门《JavaScript 引擎(V8)是如何工作的》

参考:

The Journey of JavaScript: from Downloading Scripts to Execution - Part I

Jojo
126 声望12 粉丝

Stick a little bit more every day