目录

🥡 JavaScript 文档和资源加载

# 。页面生命周期

HTML 页面的生命周期包含三个重要事件:

  • DOMContentLoaded :浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成。
  • load :浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。
  • beforeunload / unload : 当用户正在离开页面时。

每个事件的详细用途:

  • DOMContentLoaded 事件:DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。
  • load 事件: 外部资源已加载完成,样式已被应用,图片大小也已知了。
  • beforeunload 事件:用户正在离开,可以检查用户否保存了更改,并询问他是否真的要离开。
  • unload :用户几乎已经离开了,但是仍然可以启动一些操作,例如发送统计数据。

# DOMContentLoaded

DOMContentLoaded 事件发生在 document 对象上,必须使用 addEventListener 来捕获它:

document.addEventListener("DOMContentLoaded", ready);
1

🌰 例子:

<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">
1
2
3
4
5
6
7
8
9
10
11
12

DOMContentLoaded 处理程序在文档加载完成后触发,所以它可以查看所有元素,包括它下面的 <img> 元素。

但是,它不会等待图片加载。因此, alert 显示其大小为零。

# 脚本

当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束。

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

<script>
  document.addEventListener("DOMContentLoaded", () => {
    alert("DOM ready!");
  });
</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"></script>

<script>
  alert("Library loaded, inline script executed");
</script>
1
2
3
4
5
6
7
8
9
10
11

所以先显示 Library loaded ,再显示 DOM ready

提示

两个例外:

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

# 样式

外部样式表不会影响 DOM,因此 DOMContentLoaded 不会等待它们。但是如果样式后面有一个脚本,那么该脚本必须等待样式表加载完成:

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

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

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

# 浏览器表单自动填充

🌰 例子:
如果页面有一个带有登录名和密码的表单,并且浏览器记住了这些值,那么在 DOMContentLoaded 上,浏览器会尝试自动填充它们(如果得到了用户允许)。

如果 DOMContentLoaded 被需要加载很长时间的脚本延迟触发,那么自动填充也会等待。

某些网站中,登录名 / 密码字段不会立即自动填充,而是在页面被完全加载前会延迟填充。这实际上是 DOMContentLoaded 事件之前的延迟。

# window.onload

当整个页面,包括样式、图片和其他资源被加载完成时,会触发 window 对象上的 load 事件。可以通过 onload 属性获取此事件。

🌰 例子:

<script>
  window.onload = function() { // 也可以用 window.addEventListener('load', (event) => {
    alert('Page loaded');

    // 此时图片已经加载完成
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  };
</script>

<img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
1
2
3
4
5
6
7
8
9
10

这个例子会正确显示了图片大小,因为 window.onload 会等待所有图片加载完毕。

# window.onunload

当访问者离开页面时, window 对象上的 unload 事件就会被触发。可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。

特殊情况:发送分析数据:假设我们收集有关页面使用情况的数据:鼠标点击,滚动,被查看的页面区域等。当用户要离开的时候,我们希望通过 unload 事件将数据保存到我们的服务器上。

特殊的 navigator.sendBeacon(url, data) 方法可以满足这种需求,它在后台发送数据,转换到另外一个页面不会有延迟:浏览器离开页面,但仍然在执行 sendBeacon

🌰 例子:

let analyticsData = { /* 带有收集的数据的对象 */ };

window.addEventListener("unload", function() {
  navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
1
2
3
4
5
  • 请求以 POST 方式发送。
  • 不仅能发送字符串,还能发送表单以及其他格式的数据。
  • 数据大小限制在 64kb。

sendBeacon 请求完成时,浏览器可能已经离开了文档,所以就无法获取服务器响应。

还有一个 keep-alive 标志,该标志用于在 fetch (opens new window) 方法中为通用的网络请求执行此类 “离开页面后” 的请求。

如果要取消跳转到另一页面的操作,以使用另一个事件 onbeforeunload

, 浏览器内对 unload 内使用 fetch 等 api 发送统计数据的实现并不好,可能有 bug 导致统计数据失准。所以 MDN 里已经不推荐这样做了 https://volument.com/blog/s... (opens new window)

# window.onbeforeunload

如果访问者触发了离开页面的导航或试图关闭窗口, beforeunload 处理程序将要求进行更多确认。

如果要取消事件,浏览器会询问用户是否确定。

# readyState

在某些情况下,不确定文档是否已经准备就绪。如果希望的函数在 DOM 加载完成时执行,无论现在还是以后。

document.readyState 属性可以为提供当前加载状态的信息。三个可能的值:

  • loading :文档正在被加载。
  • interactive :文档被全部读取。
  • complete :文档被全部读取,并且所有资源(例如图片等)都已加载完成。

🌰 例子:

function work() { /*...*/ }

if (document.readyState == 'loading') {
  // 仍在加载,等待事件
  document.addEventListener('DOMContentLoaded', work);
} else {
  // DOM 已就绪!
  work();
}
1
2
3
4
5
6
7
8
9

检查 document.readyState 并设置一个处理程序,或在代码准备就绪时立即执行它。

readystatechange 事件,会在状态发生改变时触发,因此可以打印所有这些状态:

// 当前状态
console.log(document.readyState);

// 状态改变时打印它
document.addEventListener('readystatechange', () => console.log(document.readyState));
1
2
3
4
5

readystatechange 事件是跟踪文档加载状态的另一种机制,它很早就存在了。现在则很少被使用。

# 生命周期总结

页面的生命周期事件:

  • DOMContentLoaded 事件: DOM 准备就绪时事件就会被触发。在这个阶段,可以将 JavaScript 应用于元素。

    • 诸如 <script>...</script><script src="..."></script> 之类的脚本会阻塞 DOMContentLoaded ,浏览器将等待它们执行结束。
    • 图片和其他资源仍然可以继续被加载。
  • window.onload 事件:当页面和所有资源都加载完成时触发。

  • window.beforeunload 事件:在用户想要离开页面时。常用于要取消这个事件,浏览器就会询问我们是否真的要离开(例如,有未保存的更改)。

  • window.unload 事件:处理程序中,只能执行不涉及延迟或询问用户的简单操作。正是由于这个限制,它很少被使用。可以使用 navigator.sendBeacon 来发送网络请求。

获取当前文档的状态,使用 document.readyState 。可以在 readystatechange 事件中跟踪状态更改:

  • loading :文档正在被加载。
  • interactive :文档已被解析完成,与 DOMContentLoaded 几乎同时发生,但是在 DOMContentLoaded 之前发生。
  • complete :文档和资源均已加载完成,与 window.onload 几乎同时发生,但是在 window.onload 之前发生。

# 脚本加载类型 / 顺序

在现代的网站中,脚本往往体量很大,需要长事件处理。

当浏览器加载 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>
    
    <!-- This isn't visible until the script loads -->
    <p>...content after script...</p>
    
    1
    2
    3
    4
    5
    6

解决方法:将脚本放在页面的底部。这样脚本可以访问元素的同时,不会阻塞页面的加载。

但是这种解决方案远非完美。例如,浏览器只有在下载了完整的 HTML 文档之后才会注意到该脚本(并且可以开始下载它)。对于长的 HTML 文档来说,这样可能会造成明显的延迟。

<script> 的两个特性可以解决这两个问题: defer / async

# defer

这个特性用于告诉浏览器 不要等待脚本的加载。浏览器可以继续处理 DOM,构建 DOM,同时脚本在 「后台下载」,等 DOM 构建完成后,脚本才会执行。

🌰 例子:

<p>...content before script...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready after defer!"));
</script>

<script defer src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>

<!-- 立即可见 -->
<p>...content after script...</p>
1
2
3
4
5
6
7
8
9
10

使用 defer<script>

  • 脚本不会阻塞页面的加载;

  • 脚本总是等到 DOM 解析完毕,但是在 DOMContentedLoaded 事件之前执行。

  • 具有 defer 特性的脚本保持其相对顺序,就像常规脚本一样。

    例如两个 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>
    
    1
    2

    浏览器会扫描脚本,并行下载它们,以此提高性能,因此在下面的实例中,两个脚本是并行下载的,但是 defer 还会确保脚本执行的相对顺序。即使 small.js 先加载完成,它也需要等到 long.js 执行结束才会被执行。

defer 需要先加载 JavaScript 库,然后再加载依赖于它的脚本时,这可能会很有用。并且 ⚠️ defer 只能用于外部的脚本。(如果 <script> 脚本没有 src ,则会忽略 defer 特性。)

# async

async 特性与 defer 有些类似。它也能够让脚本不阻塞页面。但是在行为上二者有着重要的区别。

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

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

⚠️ async 脚本会在后台加载,在加载就绪时运行;但是 DOM 和其他脚本不会等待它们加载并且 它们也不会等待其他东西;意味着是一个在加载完成时执行的完全独立的脚本

🌰 例子:

<p>...content before scripts...</p>

<script>
  document.addEventListener('DOMContentLoaded', () => alert("DOM ready!"));
</script>

<script async src="https://javascript.info/article/script-async-defer/long.js"></script>
<script async src="https://javascript.info/article/script-async-defer/small.js"></script>

<p>...content after scripts...</p>
1
2
3
4
5
6
7
8
9
10

这个例子中的 外部脚本的引入,不会等待对方的加载,先加载完成的(可能是 small.js )先执行。并且:

  • 页面内容立刻显示出来:加载写有 async 的脚本不会阻塞页面渲染。
  • DOMContentLoaded 可能在 async 之前或之后触发,不能保证谁先谁后。
  • 异步脚本以加载优先的顺序执行。

当使用 第三方脚本 集成到页面时,使用 异步脚本加载方式,可以不必等待它们加载就完成 DOM 的加载。与 defer 相同, async 特性仅适用于外部脚本

# 动态脚本

这时一种向页面添加脚本的重要方式,可以使用 JavaScript 动态创建一个脚本,并将其附加到文档中。

🌰 例子:

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

利用 document.createElement ,当脚本被附加到文档 (*) 时,脚本就会立即开始加载。

在默认的情况下动态脚本的行为是 异步的

  • 不会等待任何东西,也会有东西等待它们;
  • 先加载完成的脚本先执行(加载优先顺序)。

但是如果显式设置 script.async = false 就可以改变这个规则。脚本此时按照 defer 方式,脚本将按照脚本在文档中的顺序执行。

🌰 例子:

function loadScript(src) {
  let script = document.createElement('script');
  script.src = src;
  script.async = false;
  document.body.append(script);
}

// long.js 先执行,因为代码中设置了 async=false
loadScript("/article/script-async-defer/long.js");
loadScript("/article/script-async-defer/small.js");
1
2
3
4
5
6
7
8
9
10

# 总结

  • defer 一般用于需要整个 DOM 的脚本,和 / 或脚本的相对执行顺序很重要的时候。
  • async 一般用于独立脚本,例如计数器或广告,这些脚本的相对执行顺序无关紧要。

# 资源加载

浏览器允许跟踪外部资源的加载,加载资源相关有两个事件:

  • onload :成功加载;
  • onerror :出现错误。

外部资源包括,图片、 iframe 、图片。

# 加载脚本

对于加载 第三方的脚本,调用其中的函数,可以动态加载:

let script = document.createElement('script');
script.src = "my.js";

document.head.append(script);
1
2
3
4

要运行该脚本中的函数,需要等待脚本的加载完成才能运行。

使用 自定义的脚本,现在一般使用 🪤JavaScript 的模块管理。第三方库依照使用说明导入。

# script.onlaod

onlaod 事件,在脚本加载并执行完成时触发。

🌰 例子:

let script = document.createElement('script');

// 可以从任意域(domain),加载任意脚本
script.src = "https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.3.0/lodash.js"
document.head.append(script);

script.onload = function() {
  // 该脚本创建了一个变量 "_"
  alert( _.VERSION ); // 显示库的版本
};
1
2
3
4
5
6
7
8
9
10

onload 中可以使用脚本中的变量,运行函数。

# script.onerror

当脚本加载期间,出现的 error 会被 onerror 事件跟踪到。

🌰 例子:

let script = document.createElement('script');
script.src = "https://example.com/404.js"; // 没有这个脚本
document.head.append(script);

script.onerror = function() {
  alert("Error loading " + this.src); // Error loading https://example.com/404.js
};
1
2
3
4
5
6
7

onerror 无法获取更多 HTTP error 的详细信息。不能知道是 404 还是 500 或者其他情况。只知道是加载失败了。

提示

onload / onerror 事件仅跟踪加载本身。在脚本处理和执行期间可能发生的 error 超出了这些事件跟踪的范围。如果脚本成功加载,则即使脚本中有编程 error,也会触发 onload 事件。如果要跟踪脚本 error,可以使用 window.onerror 全局处理程序。

# 其他资源加载

onload / onerror 事件也同样适用于 其他资源(基本上具有外部 src 的任何资源)的加载

🌰 例子:

let img = document.createElement('img');
img.src = "https://js.cx/clipart/train.gif"; 


img.onload = function() {
  alert(`Image loaded, size ${img.width}x${img.height}`);
};

img.onerror = function() {
  alert("Error occurred while loading image");
};
1
2
3
4
5
6
7
8
9
10
11

提示

注意:

  • 大多数资源在被添加到文档中后,便开始加载。但是 <img> 是个例外。它要等到获得 src 才开始加载。
  • 对于 <iframe> 来说, iframe 加载完成时会触发 iframe.onload 事件,无论是成功加载还是出现 error
📢 上次更新: 2022/09/02, 10:18:16