各属性执行时机模拟
尽管诸如 MDN
等文档都描述 async
和 defer
是异步加载,但是为了模拟证明,我用了下面三个文件:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.box {
width: 100px;
height: 100px;
}
</style>
</head>
<body></body>
<script src="https://sakuras.group/sakuras-docs/create.js"></script>
<script src="https://sakuras.group/sakuras-docs/async.js"></script>
</html>
/* create.js */
const newDiv = document.createElement('div');
newDiv.classList.add('box');
document.body.appendChild(newDiv);
// 可以在 create.js 里面复制很长的字符 比如某些库的源码,让 js 通过网络加载慢一些,或者自己用 node 创建个服务开一些接口,指定某个接口延迟返回结果
/* async.js */
const boxElement = document.querySelector(".box");
boxElement.style.backgroundColor = "blue";
将 create.js 和 async.js 放到服务器上,并且将 network 的节流模式换成 低速3G
, 然后根据下面的不同场景刷新 html 测试。
没有 async 和 defer
<script src="https://sakuras.group/sakuras-docs/create.js"></script>
<script src="https://sakuras.group/sakuras-docs/async.js"></script>
没有 defer
或 async
,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。因此先创建 DOM,再改变 box 颜色不会报错,反之调换 script 脚本加载顺序则必定报错。
有 async
<script src="https://sakuras.group/sakuras-docs/create.js"></script>
<script async src="https://sakuras.group/sakuras-docs/async.js"></script>
有 async
,async.js 会在 HTML 文档解析时并行下载(异步),并在下载完成后立即执行(暂停 HTML 解析)。由于 async.js 依赖 create.js,如果 async.js 比 create.js 先加载完毕则会立即执行,因此报错,所以是否报错取决于 async.js 会不会加载的比 create.js 快(当然 create.js 代码里执行创建 div 的操作也是异步的)。
在这里不妨设想一下,假如 create.js 和 async.js 都已经缓存过了,当我们刷新页面,显然 create.js 和 async.js 是从缓存里获取资源且耗时 0ms,此时是否应该报错?
我猜测不会报错
经反复刷新测试并没有报错(也不一定不报错只是没试出来,不知道有没有什么好的方法。。。)。
但是如果 create.js 和 async.js 调换一下顺序呢?
<script async src="https://sakuras.group/sakuras-docs/async.js"></script> <script src="https://sakuras.group/sakuras-docs/create.js"></script>
我猜想是 100% 报错,因为两个 js 都走缓存,当执行 async.js 代码的时候 div 还没有创建,此时一定报错。
但是经过测试,是有很大几率报错,仍然有几率不报错,这意味着存在当 async.js 执行代码的时候,create.js 已经执行过并且 div 已经成功创建,所以这是为什么呢?
1 可能 async.js 有几率在 create.js 后执行?
2 可能 async.js 一定在 create.js 后执行,但是 create.js 从执行到创建 div 的这个过程是异步的,不报错是因为 async.js 执行的时候 div 创建完成,报错则是 div 还未创建完成呢?
3 可能 async.js 从缓存里加载的晚,因此执行的时候 div 经过 create.js 已经创建完毕,所以不报错,报错是因为 async.js 从缓存里加载的快
我可能更倾向第三种。
有 defer
<script src="https://sakuras.group/sakuras-docs/create.js"></script>
<script defer src="https://sakuras.group/sakuras-docs/async.js"></script>
有 defer
,async.js 会在 HTML 文档解析时并行下载 (异步),但是 async.js 的执行会在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成,因此不会报错。
总结
对于 defer
,我们可以认为是将外链的 js 放在了页面底部。js 的加载不会阻塞页面的渲染和资源的加载。不过 defer 会按照原本的 js 的顺序执行,所以如果前后有依赖关系的 js 可以放心使用
async
和 defer
一样,会等待的资源不会阻塞其余资源的加载,也不会影响页面的加载。但是有一点需要注意下,在有 async
的情况下,js 一旦下载好了就会执行,所以很有可能不是按照原本的顺序来执行的。如果 js 前后有依赖性,用 async
,就很有可能出错。
动态添加的标签隐含
async
属性
Defer 和 async 的相同点
加载文件时不阻塞页面渲染
对于
inline
的script
无效使用这两个属性的脚本中不能调用
document.write
方法有脚本的
onload
的事件回调
Defer 和 async 的区别
html4.0 中定义了
defer
;html5.0 中定义了async
浏览器支持不同
加载时机:
具有
async
属性的脚本都在它下载结束之后立刻执行,同时会在 window 的 load 事件之前执行。所以就有可能出现脚本执行顺序被打乱的情况;具有
defer
属性的脚本都是在页面解析完毕之后,按照原本的顺序执行,同时会在 document 的DOMContentLoaded
之前执行。
使用这两个属性会有三种可能的情况
如果
async
为 true,那么脚本在下载完成后异步执行。如果
async
为 false,defer
为true,那么脚本会在页面解析完毕之后执行。如果
async
和defer
都为 false,那么脚本会在页面解析中,停止页面解析,立刻下载并且执行
如何防止 inline script 造成阻塞
问题:
在有 inline script 情况下。下载缓慢的 CSS 可能会阻碍 inline script 的执行。而这又可能阻碍之后的 async script 下载与执行
举个例子:(这里css.php下载缓慢。)
<link rel="stylesheet" href="css1.css.php" type="text/css" />
<script src="js1.js" async></script>
<script>console.log('inline script 1 ' + (+new Date - start));</script>
<link rel="stylesheet" href="css2.css.php" type="text/css"/>
<script src="js2.js" async></script>
<script>console.log('inline script 2 ' + (+new Date - start));</script>
输出结果:
external script 1 87
inline script 1 5184
external script 2 5186
inline script 2 10208
DOMContentLoaded 10216
onload 10227
原因分析:
js1 下载执行正常。但是内联 script 和第二个外部 script 因为第一个 CSS 文件(css1.css.php)的缓慢而延迟。DOMContentLoaded 也被阻塞了。 因为内联 script 可能会请求布局信息,为了使其工作,所以要等待 css 下载应用完成。而这又会 block 后续的 async script 的加载。
解决方案:
把inline script移到页面底部。虽然这仍然阻碍渲染,但是不会阻碍页面资源的下载
使用setTimeout启动长时间执行的代码
有一种方法可以使 inline script 以非 inline 的行为处理:将src指向
data:URI
。并不需要进行base64编码
把原来的内联 script:
<script>console.log('inline script 1 ' + (+new Date - start));</script>
改写成:
<script async src="data:text/javascript,console.log%28%27inline%20script%201%20%27%20%2B%20%28%2Bnew%20Date%20-%20start%29%29%3B"></script>
这样就可以避免下载缓慢的 css 造成 inline script 对其它 script 文件下载的阻塞。
评论区