Skip to content

页面生命周期:DOMContentLoaded,load,beforeunload,unload

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

  • DOMContentLoaded —— 浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成。
  • load —— 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。
  • beforeunload/unload —— 当用户正在离开页面时。 每个事件都是有用的:
  • DOMContentLoaded 事件 —— DOM 已经就绪,因此处理程序可以查找 DOM 节点,并初始化接口。
  • load 事件 —— 外部资源已加载完成,样式已被应用,图片大小也已知了。
  • beforeunload 事件 —— 用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开。
  • unload 事件 —— 用户几乎已经离开了,但是我们仍然可以启动一些操作,例如发送统计数据。
  1. DOMContentLoaded
    1. DOMContentLoaded 事件发生在 document 对象上。
    2. 我们必须使用 addEventListener 来捕获它
  2. DOMContentLoaded 和脚本
    1. 当浏览器处理一个 HTML 文档,并在文档中遇到 <script> 标签时,就会在继续构建 DOM 之前运行它。这是一种防范措施,因为脚本可能想要修改 DOM,甚至对其执行 document.write 操作,所以 DOMContentLoaded 必须等待脚本执行结束。
    2. 不会阻塞 DOMContentLoaded 的脚本
      1. 具有 async 特性(attribute)的脚本不会阻塞 DOMContentLoaded
      2. 使用 document.createElement('script') 动态生成并添加到网页的脚本也不会阻塞 DOMContentLoaded
  3. DOMContentLoaded 和样式
    1. 外部样式表不会影响 DOM,因此 DOMContentLoaded 不会等待它们。
    2. 当 DOMContentLoaded 等待脚本时,它现在也在等待脚本前面的样式。
  4. 浏览器内建的自动填充
    1. Firefox,Chrome 和 Opera 都会在 DOMContentLoaded 中自动填充表单。
    2. 因此,如果 DOMContentLoaded 被需要加载很长时间的脚本延迟触发,那么自动填充也会等待。
  5. window.onload
    1. 当整个页面,包括样式、图片和其他资源被加载完成时,会触发 window 对象上的 load 事件。可以通过 onload 属性获取此事件。
  6. window.onunload
    1. 当访问者离开页面时,window 对象上的 unload 事件就会被触发。我们可以在那里做一些不涉及延迟的操作,例如关闭相关的弹出窗口。
    2. 当用户要离开的时候,我们希望通过 unload 事件将数据保存到我们的服务器上。有一个特殊的 navigator.sendBeacon(url, data) 方法可以满足这种需求,详见规范 https://w3c.github.io/beacon/
  7. window.onbeforeunload
    1. 如果访问者触发了离开页面的导航(navigation)或试图关闭窗口,beforeunload 处理程序将要求进行更多确认。
    2. 如果我们要取消事件,浏览器会询问用户是否确定。
    3. event.preventDefault() 在 beforeunload 处理程序中不起作用
      1. 大多数浏览器都会忽略 event.preventDefault()
  8. readyState
    1. 如果我们在文档加载完成之后设置 DOMContentLoaded 事件处理程序,它永远不会运行
    2. document.readyState 属性可以为我们提供当前加载状态的信息。
    3. 有 3 个可能值
      1. loading —— 文档正在被加载。
      2. interactive —— 文档被全部读取。
      3. complete —— 文档被全部读取,并且所有资源(例如图片等)都已加载完成。

脚本:async,defer

当浏览器加载 HTML 时遇到 <script>...</script> 标签,浏览器就不能继续构建 DOM。它必须立刻执行此脚本。对于外部脚本 <script src="..."></script> 也是一样的:浏览器必须等脚本下载完,并执行结束,之后才能继续处理剩余的页面。 这会导致两个重要的问题:

  1. 脚本不能访问到位于它们下面的 DOM 元素,因此,脚本无法给它们添加处理程序等。
  2. 如果页面顶部有一个笨重的脚本,它会“阻塞页面”。在该脚本下载并执行结束前,用户都不能看到页面 一些解决办法。例如,我们可以把脚本放在页面底部。此时,它可以访问到它上面的元素,并且不会阻塞页面显示内容
html
<body>
  ...all content is above the script...

  <script src="https://javascript.info/article/script-async-defer/long.js?speed=1"></script>
</body>
  1. defer
    1. defer 特性告诉浏览器不要等待脚本。相反,浏览器将继续处理 HTML,构建 DOM。脚本会“在后台”下载,然后等 DOM 构建完成后,脚本才会执行。
    2. 具有 defer 特性的脚本不会阻塞页面。
    3. 具有 defer 特性的脚本总是要等到 DOM 解析完毕,但在 DOMContentLoaded 事件之前执行。
    4. 具有 defer 特性的脚本保持其相对顺序,就像常规脚本一样。
    5. defer 特性仅适用于外部脚本
    6. 如果 <script> 脚本没有 src,则会忽略 defer 特性。
  2. async
    1. async 特性与 defer 有些类似。它也能够让脚本不阻塞页面。但是,在行为上二者有着重要的区别。
    2. async 特性意味着脚本是完全独立的
      • 浏览器不会因 async 脚本而阻塞(与 defer 类似)。
      • 其他脚本不会等待 async 脚本加载完成,同样,async 脚本也不会等待其他脚本。
      • DOMContentLoaded 和异步脚本不会彼此等待:
        • DOMContentLoaded 可能会发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)
        • DOMContentLoaded 也可能发生在异步脚本之后(如果异步脚本很短,或者是从 HTTP 缓存中加载的)
    3. async 特性仅适用于外部脚本
  3. 动态脚本
    1. 可以使用 JavaScript 动态地创建一个脚本,并将其附加(append)到文档(document)中
    JavaScript
    	let script = document.createElement('script');
    	script.src = "/article/script-async-defer/long.js";
    	document.body.append(script); // (*)
    1. 默认情况下,动态脚本的行为是“异步”的。
      1. 它们不会等待任何东西,也没有什么东西会等它们。
      2. 先加载完成的脚本先执行(“加载优先”顺序)。

资源加载:onload,onerror

浏览器允许我们跟踪外部资源的加载 —— 脚本,iframe,图片等。

  • onload —— 成功加载,
  • onerror —— 出现 error。
  1. 加载脚本
    1. 假设我们需要加载第三方脚本,并调用其中的函数。
    JavaScript
    	let script = document.createElement('script');
    	script.src = "my.js";
    	
    	document.head.append(script);
    1. 对于我们自己的脚本,可以使用 JavaScript module,但是它们并未被广泛应用于第三方库。
    2. script.onload
      1. load 事件会在脚本加载并执行完成时触发。
      JavaScript
      	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 ); // 显示库的版本
      	};
    3. script.onerror
      1. 发生在脚本加载期间的 error 会被 error 事件跟踪到。
      JavaScript
      	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. onload/onerror 事件仅跟踪加载本身。
        1. 在脚本处理和执行期间可能发生的 error 超出了这些事件跟踪的范围。也就是说:如果脚本成功加载,则即使脚本中有编程 error,也会触发 onload 事件。如果要跟踪脚本 error,可以使用 window.onerror 全局处理程序。
  2. 其他资源
    1. load 和 error 事件也适用于其他资源,基本上(basically)适用于具有外部 src 的任何资源。
      JavaScript
      	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");
      	};
    2. 注意事项
      1. 大多数资源在被添加到文档中后,便开始加载。但是 <img> 是个例外。它要等到获得 src (*) 后才开始加载。
      2. 对于 <iframe> 来说,iframe 加载完成时会触发 iframe.onload 事件,无论是成功加载还是出现 error。
  3. 跨源策略
    1. 来自一个网站的脚本无法访问其他网站的内容。例如,位于 https://facebook.com 的脚本无法读取位于 https://gmail.com 的用户邮箱。
    2. 更确切地说,一个源(域/端口/协议三者)无法获取另一个源(origin)的内容。因此,即使我们有一个子域,或者仅仅是另一个端口,这都是不同的源,彼此无法相互访问。
    3. 如果我们使用的是来自其他域的脚本,并且该脚本中存在 error,那么我们无法获取 error 的详细信息。
    4. 要允许跨源访问,<script> 标签需要具有 crossorigin 特性(attribute),并且远程服务器必须提供特殊的 header。
    5. 三个级别的跨源访问
      1. 无 crossorigin 特性 —— 禁止访问。
      2. crossorigin="anonymous" —— 如果服务器的响应带有包含 * 或我们的源(origin)的 header Access-Control-Allow-Origin,则允许访问。浏览器不会将授权信息和 cookie 发送到远程服务器。
      3. crossorigin="use-credentials" —— 如果服务器发送回带有我们的源的 header Access-Control-Allow-Origin 和 Access-Control-Allow-Credentials: true,则允许访问。浏览器会将授权信息和 cookie 发送到远程服务器。
JavaScript
<script>
window.onerror = function(message, url, line, col, errorObj) {
  alert(`${message}\n${url}, ${line}:${col}`);
};
</script>
<script crossorigin="anonymous" src="https://cors.javascript.info/article/onload-onerror/crossorigin/error.js"></script>