页面加载事件 And 脚本加载:async, defer

871 阅读4分钟

DOMContentLoaded

时机:dom加载完成,但image,css外部资源可能尚未加载完成

应用: DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。

DOMContentLoaded 事件发生在 document 对象上。

document.addEventListener("DOMContentLoaded", ready);
<script>
  function ready() {
    alert('DOM is ready');

    // 图片目前尚未加载完成(除非已经被缓存),所以图片的大小为 0x0
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  }

  document.addEventListener("DOMContentLoaded", ready);
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">

Loaded

时机:浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。 应用:外部资源已加载完成,样式已被应用,图片大小也已知了。

DOMContentLoaded 和脚本

当浏览器处理一个 HTML 文档,并在文档中遇到

因此,DOMContentLoaded 肯定在下面的这些脚本执行结束之后发生:

此规则有两个例外:

具有 async 特性(attribute)的脚本不会阻塞 DOMContentLoaded,稍后 我们会讲到。 使用 document.createElement('script') 动态生成并添加到网页的脚本也不会阻塞 DOMContentLoaded。

DOMContentLoaded 和样式

外部样式表不会影响 DOM,因此 DOMContentLoaded 不会等待它们。

但这里有一个陷阱。如果在样式后面有一个脚本,那么该脚本必须等待样式表加载完成:

<link type="text/css" rel="stylesheet" href="style.css">
<script>
  // 在样式表加载完成之前,脚本都不会执行
  alert(getComputedStyle(document.body).marginTop);
</script>

原因是,脚本可能想要获取元素的坐标和其他与样式相关的属性,如上例所示。因此,它必须等待样式加载完成。

当 DOMContentLoaded 等待脚本时,它现在也在等待脚本前面的样式。

总结

页面生命周期事件:

  • 当 DOM 准备就绪时,document 上的 DOMContentLoaded 事件就会被触发。在这个阶段,我们可以将 JavaScript 应用于元素。 诸如 <script>...</script><script src="..."></script> 之类的脚本会阻塞 DOMContentLoaded,浏览器将等待它们执行结束。 图片和其他资源仍然可以继续被加载。
  • 当页面和所有资源都加载完成时,window 上的 load 事件就会被触发。我们很少使用它,因为通常无需等待那么长时间。
  • 当用户想要离开页面时,window 上的 beforeunload 事件就会被触发。如果我们取消这个事件,浏览器就会询问我们是否真的要离开(例如,我们有未保存的更改)。
  • 当用户最终离开时,window 上的 unload 事件就会被触发。在处理程序中,我们只能执行不涉及延迟或询问用户的简单操作。正是由于这个限制,它很少被使用。我们可以使用 navigator.sendBeacon 来发送网络请求。
    • document.readyState 是文档的当前状态,可以在 readystatechange 事件中跟踪状态更改:
    • loading —— 文档正在被加载。
    • interactive —— 文档已被解析完成,与 DOMContentLoaded 几乎同时发生,但是在 DOMContentLoaded 之前发生。
    • complete —— 文档和资源均已加载完成,与 window.onload 几乎同时发生,但是在 window.onload 之前发生。

脚本加载

defer

  • 具有 defer 特性的脚本不会阻塞页面。
  • 具有 defer 特性的脚本总是要等到 DOM 解析完毕,但在 DOMContentLoaded 事件之前执行
  • 具有 defer 特性的脚本保持其相对顺序,就像常规脚本一样。
  • !!! defer 特性仅适用于外部脚本

async

async 特性意味着脚本是完全独立的:

  • 浏览器不会因 async 脚本而阻塞(与 defer 类似)。
  • 其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。
  • DOMContentLoaded 和异步脚本不会彼此等待:
  • DOMContentLoaded 可能会发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)
  • DOMContentLoaded 也可能发生在异步脚本之后(如果异步脚本很短,或者是从 HTTP 缓存中加载的)

当我们将独立的第三方脚本集成到页面时,此时采用异步加载方式是非常棒的:计数器,广告等,因为它们不依赖于我们的脚本,我们的脚本也不应该等待它们:

动态脚本

此外,还有一种向页面添加脚本的重要的方式。

我们可以使用 JavaScript 动态地创建一个脚本,并将其附加(append)到文档(document)中:

let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)

!!! 默认情况下,动态脚本的行为是“异步”的。

模块脚本

<script type="module"></script>

模块脚本 总是 被延迟的,与 defer 特性(在 脚本:async,defer 一章中描述的)对外部脚本和内联脚本(inline script)的影响相同。

下载外部模块脚本

Async 可用于内联脚本。

总结

在实际开发中,defer 用于需要整个 DOM 的脚本,和/或脚本的相对执行顺序很重要的时候。

async 用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。