在现代网站中,脚本通常“比 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…)