1、why 两者
从事前端的小伙伴应该都知道,浏览器的渲染引擎负责解析服务器返回的一些 html、css 等资源,这些资源在未被解析之前都是以文本的形式存在,需要浏览器将对应的 html、css 解析成对应的 DOM、CSSOM 树,然后会将 DOM 和 CSSOM 合成为一棵 render 树,再经过 布局、绘制、合成 三个步骤将页面最总显示出来。
有一个问题,就是在浏览器解析 html 的过程中,如果遇到 <script> 标签的时候,浏览器就会停止解析 html,转而去执行 <script> 标签中的脚本,如果脚本中含有网络请求,那就会等网络请求完后再执行脚本中的代码,然后再回头继续解析 html。这样就造成了 html 解析被阻塞,从而导致页面渲染变慢。更严重是,如果在脚本中网络请求的资源恰好在一台网络情况较差的服务器上,这样整个网页的加载都会收到很大的影响。
举个例子:如果页面的 html 中,嵌入了一个 <script src="a.js"></script> 的标签,浏览器在解析到该便签时主要会做以下几个步骤:1. 停止解析 html、2.执行 a.js 中的脚本、3.如果 a.js 中有其他资源,则继续请求并执行、4.继续解析 html,由此就阻碍了 html 的解析。
为了针对上述的问题,就出现了 async 和 defer 两种解决方式。首先简要介绍一下:
两者都会并行下载 js,不会影响页面的解析,不同的是:
- defer 会按照顺序在 DOMContentLoaded 前按照页面出现顺序依次执行脚本。
- async 则是下载完立即执行。
- 如果遇到 js 脚本中有 document.write(),则在上述两者的使用时,浏览器会发出警告。
2、async
对于普通脚本,如果存在 async
属性,那么普通脚本会被并行请求,并尽快解析和执行。
对于模块脚本,如果存在 async
属性,那么脚本及其所有依赖都会在延缓队列中执行,因此它们会被并行请求,并尽快解析和执行。
该属性能够消除解析阻塞的 Javascript。举个例子:
<script src="b.js"async></script>
<script src="c.js"async></script>
- 不阻止解析 document, 并行下载 b.js,c.js
- 当脚本下载完后立即执行,两者执行顺序不确定,执行阶段不确定,可能在
DOMContentLoaded
事件前或者后
3、defer
这个布尔属性被设定用来通知浏览器该脚本将在文档完成解析后,触发 DOMContentLoaded
事件前执行。
有 defer
属性的脚本会阻止 DOMContentLoaded
事件,直到脚本被加载并且解析完成。举个例子:
<script src="d.js" defer></script>
<script src="e.js" defer></script>
- 不阻止解析 document, 并行下载 d.js,e.js
- 即使下载完 d.js, e.js 仍继续解析 document
- 按照页面中出现的顺序,在其他同步脚本执行后,
DOMContentLoaded
事件前 依次执行 d.js,e.js
4、总结
<script src="script.js"></script>:没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即“ 指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script async src="script.js"></script>:有 async,在加载和渲染后续文档元素的过程时,将和 script.js 的加载与执行并行进行(异步),如果脚本中涉及操作 DOM 的操作就可能出现问题(DOM 还没解析完成)。
<script defer src="myscript.js"></script>:有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DoMContentLoaded 事件触发之前完成,因此 defer 适合与 DOM 有关联的脚本。
注意:
- 如果 script 无 src 属性,则 defer, async 会被忽略
- 动态添加的 script 标签隐含 async 属性
- 不管是 async 还是 defer,两者都只适用于外部的脚本,而且还要注意兼容性的问题,如果浏览器不能兼容,还是把 script 标签放到页面的底部比较好。