拒绝js阻塞—defer、async做用和区别分析

js阻塞原理

浏览器内核能够分红两部分:渲染引擎(Layout Engine 或者 Rendering Engine)和 JS 引擎。早期渲染引擎和 JS 引擎并无十分明确的区分,但随着 JS 引擎愈来愈独立,内核也成了渲染引擎的代称(下文咱们将沿用这种叫法)。渲染引擎又包括了 HTML 解释器、CSS 解释器、布局、网络、存储、图形、音视频、图片解码器等等零部件。 javascript

JS 引擎是独立于渲染引擎存在的。咱们的 JS 代码在文档的何处插入,就在何处执行。当 HTML 解析器遇到一个 script 标签时,它会暂停渲染过程,将控制权交给 JS 引擎。JS 引擎对内联的 JS 代码会直接执行,对外部 JS 文件还要先获取到脚本、再进行执行。等 JS 引擎运行完毕,浏览器又会把控制权还给渲染引擎,继续 CSSOM 和 DOM 的构建。 所以与其说是 JS 把 CSS 和 HTML 阻塞了,不如说是 JS 引擎抢走了渲染引擎的控制权。 html

渲染引擎碰到js就交出大权是由于他不知道js的内容会不会对接下来的渲染有没有影响。可是咱们引入js的时候是知道有没有影响的,能够根据具体状况用三种方式之一加载js。java

JS的三种加载方式

js 有三种加载方式。jquery

  1. 正常模式
<script src="script.js"></script>

没有 defer 或 async,浏览器会当即加载并执行指定的脚本,“当即”指的是在渲染该 script 标签之下的文档元素以前,也就是说不等待后续载入的文档元素,读到就加载并执行。web

  1. async模式
<script async src="script.js"></script>

有 async,script.js会被异步加载,即加载和渲染后续文档元素的过程将和 script.js 的加载并行进行(异步)。当 script.js加载完整当即执行script.js。执行script.js时,html解析暂停。
从加载完成当即执行来看,async模式 执行顺序与写的顺序无关,不保证执行顺序。segmentfault

  1. defer 模式
<script defer src="index.js"></script>

有 defer,script.js会被异步加载,即加载和渲染后续文档元素的过程将和 script.js 的加载并行进行(异步)。这一点与async模式一致。
不一样的是当 script.js加载完成并不会当即执行,而是在全部元素解析完成以后,DOMContentLoaded 事件触发以前完成。所以它会按照写的顺序执行。浏览器

三种方式的直观对比

一图胜千言: 原图地址 网络

来个demo

// html 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>defer-async</title>

    <script type="text/javascript" async src='./async1.js'></script>
    <script type="text/javascript" async src='./async2.js'></script>
    <script type="text/javascript" src='./normal.js'></script>
</head>
<body>
    <div id="warp">warp</div>
</body>
</html>

而后 async1.js 文件巨大(到底有多大,我是把jquery的压缩版拷进来了),而后最后加上 console.log('async1');
文件async2.jsnormal.js 中分别是 console.log('async2');console.log('normal');
打开网页控制台显示以下: async2 先加载完成就先执行了。
dom

而当我把 前面引用换成defer时异步

<script type="text/javascript" defer src='./defer1.js'></script>
    <script type="text/javascript" defer src='./defer2.js'></script>
    <script type="text/javascript" src='./normal.js'></script>

同理,defer1.js 里放了jquery的压缩版源码。defer2.js里只放了一句日志; 刷新网页看下日志:

defer1 、defer2仍是按照顺序执行的。

把async、defer都加上,

<script type="text/javascript" async src='./async1.js'></script>
    <script type="text/javascript" async src='./async2.js'></script>

    <script type="text/javascript" defer src='./defer1.js'></script>
    <script type="text/javascript" defer src='./defer2.js'></script>
    <script type="text/javascript" src='./normal.js'></script>

日志以下:

这个顺序应该不是固定的,符合normal最先,defer1会在 defer2以前的规矩。 至于async 和 defer的先后则要看自己js的加载以及dom树的构建时机吧。

三种方式适合何时用

growingwiththeweb 推荐优先级依次是 async defer normal。。

  • 当你的js是个独立的模块且不依赖任何js,使用 async;
  • 若是你的js依赖其余js或者被其余js 依赖,使用 defer;
  • 若是你对js文件很小且被 async script 依赖,使用正常模式的script且放在async script 前面。

可能的坑

虽然理论上defer按加载顺序执行,但也有同窗反映事实上并非这样。。好比这位同窗的问题:

我认为这是涉及到 event loop的 task和微任务了。
"在现实当中,延迟脚本并不必定会按照顺序执行,也不必定会在 DOMContentLoaded 事件触发前执行,所以最好只包含一个延迟脚本。" 《JavaScript 高级程序设计(第三版)》如是说,因此脚本之间有依赖,最好使用一个异步脚本吧。好比上面同窗那个问题 能够改为这样<script src="1.js"></script>.

参考资料

掘金小册
async vs defer
谈谈script标签
defer async区别