浏览器_Script 的 defer 和 async - zen0822/interview GitHub Wiki
开局一张图:
当浏览器加载 HTML
时遇到 <script>...</script>
标签,浏览器就不能继续构建 DOM
。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script>
也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。
这会导致两个重要的问题:
- 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本不能给它们添加事件等。
- 如果页面顶部有一个庞大的脚本,它会“阻塞页面”。在脚本下载并执行结束前,用户都不能看到页面内容:
<p>...content before script...</p>
<script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- 在脚本加载结束前都看不到下面的内容 -->
<p>...content after script...</p>
解决方法除了可以把脚本放在页面底部,此时,它可以访问到它上面的元素,并且不会阻塞页面显示内容。但是这样的方案绝非完美。例如:浏览器只有在下载完整的 HTML
文档后才会注意到脚本(并且开始下载它)。对于长的 HTML
文档来说,这样的延迟必须引起注意。
这里还有两个 <script>
属性可以解决我们的这个问题:defer
和 async
。
defer
属性告诉浏览器它应该继续处理页面,并在“后台”下载脚本,然后等页面处理完成后才开始执行此脚本。
- 具有
defer
属性的脚本不会阻塞页面的加载。 - 具有
defer
属性的脚本总是要等到DOM
解析完毕,但在DOMContentLoaded
事件之前执行。
页面事件的生命周期:
DOMContentLoaded
:DOM
树解析完毕,完成之后才会触发DOMContentLoaded
,此时GPU
还没开始根据CSS
树开始绘画,我们可以在这个阶段使用 JS 去访问元素。
async
和defer
的脚本可能还没有执行。- 图片及其他资源文件可能还在下载中。
load
事件在页面所有资源被加载完毕后触发,通常我们不会用到这个事件,因为我们不需要等那么久。window.load
只会执行第一次赋值的函数,无论是函数形式赋值还是标签内赋值<body onload="alert('d');">
beforeunload
在用户即将离开页面时触发,它返回一个字符串,浏览器会向用户展示并询问这个字符串以确定是否离开。unload
在用户已经离开时触发,我们在这个阶段仅可以做一些没有延迟的操作,由于种种限制,很少被使用。document.readyState
表征页面的加载状态,可以在readystatechange
中追踪页面的变化状态:
- loading —— 页面正在加载中。
- interactive —— 页面解析完毕,时间上和 DOMContentLoaded 同时发生,不过顺序在它之前。
- complete —— 页面上的资源都已加载完毕,时间上和 window.onload 同时发生,不过顺序在他之前。
下面的例子证明上述情况:
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!")); // (2)
</script>
<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
<!-- 立即可见 -->
<p>...content after scripts...</p>
- 页面内容立即显示。
-
DOMContentLoaded
在等待defer
脚本动作的完成。它仅在脚本 (2) 下载且执行结束后才被触发。
Defer 脚本保持他们的相对顺序,就像常规脚本一样。 所以,如果我们有一个长脚本在前,一个短脚本在后,那么后者就会等待前者。
<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
浏览器解析页面找到
script
属性并并行下载它们,以提高性能。因此,在上面的实例中,两个脚本并行下载。small.js
可能会先下载完成。但是规范要求脚本按照文档顺序执行,因此它要等到long.js
执行结束才会被执行。
defer
属性会忽略没有src
属性的<script>
脚本。
async
属性意味着脚本是完全独立的:
- 页面不会等待异步脚本下载,它会继续处理页面并显示内容,但是会等待脚本执行。
-
DOMContentLoaded
和async
脚本不会彼此等待:-
DOMContentLoaded
可能发生在异步脚本之前(此时异步脚本在页面加载完成后才加载完成) -
DOMContentLoaded
也可能发生在异步脚本之后(此时异步脚本可能很短或者是从HTTP
缓存中加载的)
-
- 其他脚本不会等待
async
脚本加载完成,同样async
脚本也不会等待其他脚本。
我们也可以使用 JavaScript 动态地添加脚本:
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
当脚本附加到文档 (*) 时,脚本就会开始加载:
默认情况下,动态脚本表现为“异步”行为。
这也就是说:
- 它们不会等待其他内容,其他的内容也不会等待它们。
- 先加载完成的脚本先运行(“加载优先” 顺序)
我们可以通过将 async 属性显示修改为 false 以将加载优先顺序修改为文档顺序(就像常规脚本一样)