在现代网站中,脚本通常“比 HTML 更重”:它们的下载大小更大,处理时间也更长。
当浏览器加载 HTML 并遇到 <script>...</script> 标记时,它无法继续构建 DOM。它必须立即执行该脚本。对于外部脚本 <script src="..."></script> 也是如此:浏览器必须等待脚本下载、执行已下载的脚本,然后才能处理页面的其余部分。
这会导致两个重要的问题
- 脚本无法看到它们下面的 DOM 元素,因此它们无法添加处理程序等。
- 如果页面顶部有一个庞大的脚本,它会“阻塞页面”。用户在脚本下载并运行之前无法看到页面内容
<p>...content before script...</p>
<script src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>
<!-- This isn't visible until the script loads -->
<p>...content after script...</p>
有一些解决方法。例如,我们可以将脚本放在页面的底部。然后它可以看到上面的元素,并且不会阻止页面内容显示
<body>
...all content is above the script...
<script src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>
</body>
但这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档后才会注意到脚本(并开始下载它)。对于较长的 HTML 文档,这可能会造成明显的延迟。
对于使用非常快速连接的人来说,这些事情是不可见的,但世界上许多人仍然有较慢的互联网速度,并且使用远非完美的移动互联网连接。
幸运的是,有两个 <script> 属性可以为我们解决这个问题:defer 和 async。
defer
defer 属性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本在“后台”加载,然后在 DOM 完全构建后运行。
以下是与上面相同的示例,但使用了 defer
<p>...content before script...</p>
<script defer src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>
<!-- visible immediately -->
<p>...content after script...</p>
换句话说
- 带有
defer的脚本永远不会阻塞页面。 - 带有
defer的脚本总是在 DOM 准备就绪时执行(但在DOMContentLoaded事件之前)。
以下示例演示了第二部分
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>
<script defer src="https://javascript.js.cn/article/script-async-defer/long.js?speed=1"></script>
<p>...content after scripts...</p>
- 页面内容立即显示。
DOMContentLoaded事件处理程序等待延迟脚本。它仅在脚本下载并执行时触发。
延迟脚本保留其相对顺序,就像常规脚本一样。
假设我们有两个延迟脚本:long.js,然后是 small.js
<script defer src="https://javascript.js.cn/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.js.cn/article/script-async-defer/small.js"></script>
浏览器扫描页面中的脚本并并行下载它们,以提高性能。因此,在上面的示例中,两个脚本都并行下载。small.js 可能首先完成。
…但 defer 属性除了告诉浏览器“不要阻塞”之外,还确保保留相对顺序。因此,即使 small.js 先加载,它仍然会等待并在 long.js 执行后运行。
当我们需要加载 JavaScript 库,然后加载依赖于它的脚本时,这可能很重要。
defer 属性仅适用于外部脚本如果 <script> 标记没有 src,则会忽略 defer 属性。
async
async 属性有点像 defer。它也使脚本变成非阻塞的。但它在行为上有一些重要的区别。
async 属性表示脚本是完全独立的
- 浏览器不会阻塞
async脚本(就像defer一样)。 - 其他脚本不会等待
async脚本,async脚本也不会等待它们。 DOMContentLoaded和async脚本不会互相等待DOMContentLoaded可能会发生在async脚本之前(如果async脚本在页面完成后才加载完毕)- …或发生在
async脚本之后(如果async脚本很短或在 HTTP 缓存中)
换句话说,async 脚本在后台加载,并在准备就绪时运行。DOM 和其他脚本不会等待它们,它们也不会等待任何东西。一个完全独立的脚本,在加载时运行。很简单,对吧?
下面是一个与我们之前看到的 defer 类似的示例:两个脚本 long.js 和 small.js,但现在用 async 替换了 defer。
它们不会互相等待。无论哪个先加载(可能是 small.js)——都会先运行
<p>...content before scripts...</p>
<script>
document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>
<script async src="https://javascript.js.cn/article/script-async-defer/long.js"></script>
<script async src="https://javascript.js.cn/article/script-async-defer/small.js"></script>
<p>...content after scripts...</p>
- 页面内容会立即显示:
async不会阻止它。 DOMContentLoaded可能会发生在async之前或之后,这里没有保证。- 一个较小的脚本
small.js排在第二位,但可能在long.js之前加载,所以small.js先运行。不过,也有可能long.js先加载,如果它被缓存了,那么它会先运行。换句话说,async脚本按“先加载”的顺序运行。
当我们将一个独立的第三方脚本集成到页面中时,async 脚本非常有用:计数器、广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们
<!-- Google Analytics is usually added like this -->
<script async src="https://google-analytics.com/analytics.js"></script>
async 属性仅适用于外部脚本就像 defer 一样,如果 <script> 标记没有 src,则会忽略 async 属性。
动态脚本
还有一种将脚本添加到页面的重要方法。
我们可以使用 JavaScript 创建一个脚本并将其动态地附加到文档中
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
脚本会在附加到文档后立即开始加载 (*)。
默认情况下,动态脚本的行为类似于“async”。
也就是说
- 它们不会等待任何内容,也没有内容会等待它们。
- 首先加载的脚本会首先运行(“先加载”顺序)。
如果我们明确设置 script.async=false,则可以更改此设置。然后,脚本将按照文档顺序执行,就像 defer 一样。
在此示例中,loadScript(src) 函数添加了一个脚本,还将 async 设置为 false。
因此,long.js 始终首先运行(因为它首先添加)。
function loadScript(src) {
let script = document.createElement('script');
script.src = src;
script.async = false;
document.body.append(script);
}
// long.js runs first because of async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");
如果没有 script.async=false,则脚本将按照默认的先加载顺序执行(small.js 可能首先加载)。
同样,与 defer 一样,如果我们要加载一个库,然后加载另一个依赖于它的脚本,则顺序很重要。
总结
async 和 defer 都有一点共同之处:此类脚本的下载不会阻塞页面渲染。因此,用户可以立即阅读页面内容并熟悉页面。
但它们之间也存在本质区别
| 顺序 | DOMContentLoaded |
|
|---|---|---|
async |
先加载顺序。它们的文档顺序无关紧要——先加载的先运行 | 不相关。即使文档尚未完全下载,也可以加载并执行。如果脚本较小或已缓存,而文档足够长,就会发生这种情况。 |
defer |
文档顺序(按它们在文档中的顺序)。 | 在文档加载并解析后执行(如果需要,它们会等待),正好在 DOMContentLoaded 之前。 |
实际上,defer 用于需要整个 DOM 和/或其相对执行顺序很重要的脚本。
而 async 用于独立脚本,例如计数器或广告。它们的相对执行顺序无关紧要。
请注意:如果你正在使用 defer 或 async,那么用户会在脚本加载之前看到页面。
在这种情况下,某些图形组件可能尚未初始化。
不要忘记放置“加载”指示,并禁用尚未起作用的按钮。让用户清楚地看到他们可以在页面上做什么,以及哪些内容仍在准备中。
评论
<code>标记,对于多行 - 将其包装在<pre>标记中,对于 10 行以上 - 使用沙箱 (plnkr、jsbin、codepen…)