kooboo

HTML标签:script 标签

后面我们会学习 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 文件的什么位置较好

1、script 标签的合适位置

首先,<script> 既可以放在 head 区域,也可以放在 body 区域。

1.1 head 区域

1.1.1 先看错误示例

<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 内(这点我们下面再讲)。

1.1.2 建议使用 defer 属性

从 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。

1.1.3 使用 window.onload

上面提到,defer 属性只支持引入外部 js 脚本,不支持直接在 script 里内嵌 js 代码的情况。假如我坚持要在head 区域写 js,那我们可以用window.onload

<head>
  window.onload = function(){
    // 这里可以写操作界面元素的JS代码,等页面加载完毕后再执行
    ...
  }
</head>

window.onload是一个 DOM API(我们在 JavaScript 篇章会介绍),意思是:等界面上所有内容都加载完毕后(不仅要等文本加载完毕,而且要等图片也要加载完毕),再执行{}中的代码。做到了先加载,最后执行,也就是:等页面加载完毕后再执行

当然,我们可以根据具体需求,将 window.onload 写在 <head> 标签中,或者写在 <script> 标签中。

1.2 body 区域

1.2.1 script 最好放在 body 最底部

一般来说,假如想在 <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。

1.2.2 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

2、对于 type="module"

在主流的现代浏览器中,<script> 的属性可以加上 type="module"

这时浏览器会认为这个文件是一个JavaScript 模块,其中的解析规则、执行环境会略有不同; 这时 <script> 的默认行为会像是 defer 一样,在后台下载,并且等待 DOM 解析、渲染完成之后才会执行,所以 defer 属性无法在 type="module" 的情况下发生作用。

但同样可以通过 async 属性使它在下载完成后即刻执行。

3、总结

(1)需要注意 script 的摆放位置,最好放在 body 最底部。

(2)defer 由于后台载入、不打断渲染、以及确保执行顺序的特点,基本上在没特殊需求的情况下,在