{{msg}}
后面我们会学习 javascript,我们可以在 .js 文件书写 js 代码,也可以在 .html 文件里用 <script>
标签包裹 js 代码,比如:
<script type="text/javascript">
// js 代码:在控制台输出 "hello world"
console.log("hello world")
</script>
我们要知道,浏览器解析 HTML 是一行一行按顺序读取的,当浏览器读到 <script>
时,便会 暂停解析 html。
那么,为了防止当下载、执行 <script>
中的资源时,用户卡在了空白画面(html 被阻塞,没能渲染 html 页面),产生网站太慢、不好用的糟糕体验,我们需要 思考 script 标签放在 html 文件的什么位置较好。
首先,<script>
既可以放在 head 区域,也可以放在 body 区域。
<head>
<script type="text/javascript">
// js 代码:在控制台输出 "hello world"
console.log("hello world")
</script>
</head>
<body>
<!-- 展示 html 页面 -->
</body>
上面把 <script>
放在 head 区域,并且直接在 <script>
里写 js 代码。前面说过,浏览器解析 HTML 是一行一行按顺序读取的,当浏览器读到 <script>
时,便会暂停解析 html。
也就是说,在解析下载 html 页面内容(body 区域)前,会先下载执行 <script>
里的这段 js 代码。
这么做,会有几个问题: (1)可能会导致页面的阻塞、卡顿。因为浏览器 js 引擎是单线程的,最多只能同时加载两个 js,并且下载 js 时还会停止解析 html,假如 js 内容比较大、执行耗时长,就会导致 html 页面长时间排队、没有等到渲染,出现明显的卡顿、内容滞后,对用户体验很不友好。
(2)假如 js 涉及 dom 操作,上面说到,html 的解析会被阻塞,意味着页面的 dom 元素(暂时可以理解成 html 标签)也没有完全加载,这时候浏览器就会觉得懵逼:dom 都没加载,我上哪去操作 dom?所以会 出现 bug,控制台报错。
事实上,假如想在 .html 文件里写 js 代码,我们通常会 把 script 标签放在 body 内(这点我们下面再讲)。
从 HTML4 开始,script 多了 defer
属性,HTML5 则多了 async
属性(下面会讲),两者都是用来帮助开发者控制 <script>
内资源的载入及执行顺序,以及避免 DOM 的解析(即渲染 html 页面)因资源下载而暂停。
在 head 区域使用 script 标签,建议配合使用 defer 属性。比如,我们可以用 script 引入外部的 js 文件,src 即文件的路径:
<head>
<script src="1.js" defer></script>
</head>
<body>
</body>
defer
的意思是延迟(Deferred),先下载脚本,再延迟执行脚本。上面案例就是常使用的情况:将业务代码脚本加上 defer
属性,放到更上层的 head 标签下。
这也是 HtmlWebpackPlugin 插件的默认引入打包脚本的方式,后面我们会了解到的。
defer 是布尔属性,默认为 true。于是,浏览器会先下载 1.js,然后继续解析、渲染 html 画面(不会因为需要载入 <script>
内的资源而卡住),等 html 渲染完成,才会执行 1.js,最后再加载图片资源、css 文件等。
需要注意两点:
(1)可惜的是,defer 属性只支持引入外部 js 脚本,对于 script 标签内的嵌套 js 代码不生效。
(2)如果多个 script 设置了 defer 属性,这几个 script 的执行顺序和声明顺序相同。并不是谁先下载谁先执行,以下面案例为例:
<script src="1.js" defer></script>
<script src="2.js" defer></script>
无论 1.js 和 2.js 无论谁先下载完,最终都是先执行 1.js,再执行 2.js。
上面提到,defer
属性只支持引入外部 js 脚本,不支持直接在 script
里内嵌 js 代码的情况。假如我坚持要在head
区域写 js,那我们可以用window.onload
。
<head>
window.onload = function(){
// 这里可以写操作界面元素的JS代码,等页面加载完毕后再执行
...
}
</head>
window.onload
是一个 DOM API(我们在 JavaScript 篇章会介绍),意思是:等界面上所有内容都加载完毕后(不仅要等文本加载完毕,而且要等图片也要加载完毕),再执行{}
中的代码。做到了先加载,最后执行,也就是:等页面加载完毕后再执行。
当然,我们可以根据具体需求,将 window.onload
写在 <head>
标签中,或者写在 <script>
标签中。
一般来说,假如想在 <script>
标签内书写 js 代码,那么最好放在 body 区域的最底部(</body>
之前),防止踩前面错误示例的坑:
<body>
<!-- 这里省略一些 html 标签 -->
<!-- script 放在 body 区域的底部 -->
<script type="text/javascript">
</script>
</body>
这样做的好处是,浏览器会先加载解析 body 区域的 html 标签,再加载 js 代码,这就不会造成 html 页面的卡顿。 但也还是有缺点:
(1)在复杂的网站中, HTML、JavaScript 的个头都很大,需要等到整个 DOM 树都载入完成才开始下载 <script>
内的资源,从网站读取完成到可操作,会产生明显的延迟感。
(2)对于那些高度依赖 js 交互的页面来说,js 不应该总是最后才加载。
对于这些页面,我们可以一边下载解析 html,一边加载 js,这需要用到 <script>
标签的两个属性:defer、async。
defer 上面已经介绍过,在 body 区域用法也是一样。这里再介绍下 async。
async:脚本一旦被下载就会被执行,不管什么时机。同样只适用于引入外部 js 文件。
<script>
标签加上 async
属性后,与 defer
的相同点是也会在后台执行下载,但不同的是,当下载完成会马上暂停 DOM 解析(如果还没有解析完成的话),并开始执行 JavaScript。因为下载完成后会立即执行,加上 async
属性后,就无法保证执行顺序了。
注意:
(1)适合于执行顺序无关的脚本,比如广告、网站流量分析脚本,比如插入 Google 分析脚本:
<script async src="//www.google-analytics.com/analytics.js"></script>
(2)谁先下载完,就先执行谁。比如我们同时引入两个 js,那么:1.js 和 2.js 谁先执行是未知的,要看谁先下载完。
<script type="text/javascript" src="1.js" async></script>
<script type="text/javascript" src="2.js" async></script>
(3)这个属性,也支持通过 JavaScript 动态创建 <script>
的情况。例如:
const script = document.createElement('script')
script.src = "3.js"
document.body.append(script)
动态创建的 <script>
,默认就是异步载入(相当于 async
为 true)。但在 HTML5 标准中,可以通过设定属性 async
将异 步载入关闭:
script.async = false
在主流的现代浏览器中,<script>
的属性可以加上 type="module"
。
这时浏览器会认为这个文件是一个JavaScript 模块,其中的解析规则、执行环境会略有不同;
这时 <script>
的默认行为会像是 defer
一样,在后台下载,并且等待 DOM 解析、渲染完成之后才会执行,所以 defer
属性无法在 type="module"
的情况下发生作用。
但同样可以通过 async
属性使它在下载完成后即刻执行。
(1)需要注意 script
的摆放位置,最好放在 body
最底部。
(2)defer
由于后台载入、不打断渲染、以及确保执行顺序的特点,基本上在没特殊需求的情况下,在