1. <script>
标签
将JavaScript代码嵌入到HTML中主要方式是使用<script>
元素。
使用<script>
的方式有两种:
(1)直接在网页中嵌入JavaScript代码:
<script>
function sayHi() {
console.log("Hi");
}
</script>
包含在<script>
中的代码会被从上到下执行,在<script>
元素中的代码被计算完成之前,页面的其余内容不会被加载也不会被显示。
(2)引入外部文件中的代码,使用src
属性,该属性的值是一个URL,指向包含js代码的文件。
<script src="example.js"></script>
与解释行内的js代码一样,在解释外部的js文件时,页面也会阻塞,阻塞的时间也包含下载文件的时间(不使用defer和async的情况下)。
如果使用了src属性,就不应该在<script>
和</script>
之间再包含其他js代码。如果两者都提供的话,则浏览器只会下载并执行脚本文件,从而忽略行内代码。
在没有使用defer
或async
属性时,浏览器会按照<script>
在页面中出现的顺序依次解释它们。第二个<script>
元素的代码必须在第一个<script>
元素的代码解释完毕才能开始解释,依次类推。
2. <script>
标签位置
浏览器的渲染引擎与js引擎是互斥的,当HTML解析器遇到一个script
标签时,它就会暂停渲染过程,将控制权交给js引擎。js引擎对内联的js代码会直接执行,对外部的js文件要先获取,再执行。等js引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续DOM和CSSOM的构建。
过去,所有的script
标签都被放在页面的<head>
标签内,如下:
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="example1.js"></script>
<script src="example2.js"></script>
</head>
这种做法的主要目的是将外部CSS和JavaScript文件集中在一起。然后所有的JS文件都放在<head>
内,意味着所有的JavaScript代码都下载、解析和解释完毕后,才能开始渲染页面(页面在浏览器解析到<body>
的起始标签时开始渲染)。如果引入的脚本过多,会导致页面的渲染明显延迟,浏览器窗口完全空白。
为了解决这个问题,现在通常将所有JavaScript的引用放在<body>
元素中的页面内容后面:
<body>
......
<script src="example1.js"></script>
<script src="example2.js"></script>
</body>
这样会在处理js代码之前完全的渲染页面,用户会感觉页面加载更快了。
可以发现,没有加defer和async属性的脚本,在遇到<script>
标签时,首先开始下载脚本,从脚本开始下载到脚本完成执行,整个过程中,HTML的解析都是停止的。当脚本解析完成后,HTML解析继续。
3. <script>
标签的defer属性
defer属性只对外部的脚本文件有效,给<script>
标签添加defer属性,可以推迟脚本的执行。在遇到脚本时,还是会立即下载,但是会延迟执行。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="./scripts/a.js" defer></script>
</head>
<body>
<ul>
<li>HTML</li>
<li>CSS</li>
<li>JAVASCRIPT</li>
</ul>
</body>
</html>
上面的<script>
标签虽然包含在<head>
中,由于加上了defer属性,它们会在浏览器解析到结束的</html>
标签后才会执行。
HTML5规范要求脚本应该按照它们出现的顺序执行,因此第一个推迟的脚本会在第二个推迟的脚本之前执行,而且两者都会在DOMContentLoaded
事件之前执行。但是在实际情况下,推迟执行的脚本不一定总会按顺序执行,或者在DOMContentLoaded
事件之前执行。因此最好只包含一个这样的脚本。
可以发现,对于defer的脚本,脚本下载时,HTML仍然在解析,当HTML解析完成以后,开始执行脚本,即延迟执行。
4. <script>
标签的async属性
async属性也只适用于外部脚本,浏览器同样会立即下载脚本,但是与defer不同的是,async的脚本不能保证按照它们出现的次序执行,而且当该脚本下载完成后就会立即执行该脚本。由于脚本大小不一样,下载完成的所需要的时间不同,所以这些脚本不能保证按照出现顺序执行。
可以发现,对于async的脚本,脚本下载时,HTML仍然在解析,但是与defer不同的是,当脚本下载完成后立即就会执行脚本。
脚本下载可能很快,此时HTML还没有完成解析(DOMContentLoaded事件还没触发),首先暂停HTML解析,去执行脚本,之后继续解析。
脚本下载也可能较慢,此时HTML解析已经完成了(DOMContentLoaded事件已经触发了),直接执行脚本。
async属性的脚本会保证在页面的load事件之前执行,但是可能在DOMContentLoaded事件之前或者之后执行,由于无法确定该脚本执行时机,异步脚本不应该在加载期间操作DOM。
5. 如何使用
- 如果脚本是一个模块并且不依赖于其他任何脚本,则使用async
- 如果脚本依赖于其他脚本或者被其他脚本依赖,则使用defer
- 如果该脚本很小并且被一个async的脚本依赖,则使用内联的脚本,并且把该脚本放在async脚本的上面。
参考[async vs defer attributes](async vs defer attributes - Growing with the Web)
js高程四