【译文】html页面事件: DOMContentLoaded, load, beforeunload, unload

html页面的生命周期中有三个重要的事件:

  • DOMContentLoaded - 浏览器完全加载了 HTML,并构建了 DOM 树,但是图片 【译文】html页面事件: DOMContentLoaded, load, beforeunload, unload 和样式表等外部资源可能尚未加载

  • load - 不仅加载了html,还加载了所有外部资源:图像、样式等。

  • beforeunload/unload - 用户正在离开页面

每一个事件都可能有用:

  • DOMContentLoaded 事件 - DOM已经准备就绪,此时可以查找DOM节点

  • beforeunload 事件 - 加载外部资源,样式表内用已经应用,图像的大小已经确定

  • DOMContentLoaded 事件 - 用户正在离开:我们可以检查用户是否保存了修改,并询问他们是否确定离开

  • unload 事件 - 用户几乎已经离开,但是我们仍然可以启动一些操作,比如向服务器发送统计数据

让我们具体地对这些事件一探究竟。

DOMContentLoaded

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

document.addEventListener("DOMContentLoaded", ready);
// not "document.onDOMContentLoaded = ..."

比如:

// DOMContentLoaded事件结束后才加载图片(加载图片之前先执行DOMContentLoaded绑定的回调函数)
<script>
  function ready() {
    alert('DOM is ready');

    // image is not yet loaded (unless it was cached), so the size is 0x0
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  }

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

<img >

在上边的例子中,DOMContentLoaded 处理程序在文档加载完成时执行,所以除了【译文】html页面事件: DOMContentLoaded, load, beforeunload, unload标签其他所有元素都是可见的。

但是它不必等待图片的加载,所以 alert 直接显示图片的宽度为 0x0

第一眼看上去, DOMContentLoaded 事件很简单,DOM树准备完毕--事件触发。注意:它仍然有一些特殊之处。

DOMContentLoaded 和 scripts

当浏览器处理html文档并且遇到<script>(内联)标签时,它需要在构建dom之前去执行这段脚本。这是为了预防js可能回去修改dom,甚至将 document.write 写入html文档,所以 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>

在以上例子中,我们首先看到 "Library loaded..." 弹出,然后是 "DOM ready!"(等所有脚本执行完才弹出)

脚本不阻塞DOMContentLoaded

此规则有两个特例:

  1. 脚本有 async 属性,我们稍后会提到此属性,不会阻塞 DOMContentLoaded
  2. 脚本由 document.createElement('script') 动态生成,然后加入到html文档,也不会阻塞 DOMContentLoaded 具体看下篇:document.createElement('script') 和 DOMContentLoaded 执行顺序

DOMContentLoaded 和 styles

外链的样式表也不会影响dom,因此 DOMContentLoaded也不会等待外链css

但是这里有一个陷阱。如果我们在外链样式表后由一个script脚本,script脚本必须等待样式表加载完:

<link type="text/css" rel="stylesheet" href="style.css">
<script>
  // the script doesn't not execute until the stylesheet is loaded
  alert(getComputedStyle(document.body).marginTop);
</script>

这是因为脚本可能会去获取元素的坐标和其他样式有关的属性,像上边的例子。因此,DOMContentLoaded 必须等待css加载完

DOMContentLoaded 需要等待script加载一样,现在同样也需要等待css的加载(因为js需要等待css的加载完成,而DOMContentLoaded需要等待js的加载)。

浏览器内置的表单填充

DOMContentLoaded 上的 Firefox、Chrome 和 Opera 自动填充表单。

比如,如果页面有一个登录名和密码的表单并且浏览器记住了登录名和密码(如果用户允许),DOMContentLoaded触发时会自动填充表单。

因此 DOMContentLoaded 如果被很大的脚本加载延迟,表单自动填充也会等待。你可能在一些网站上考到过(如果你使用了浏览器的自动填充功能) - 用户名/密码字段不会立即填充,而是会等待整个页面加载完。这实际上是DOMContentLoaded 事件的延迟导致的。

window.onload

load 事件作用在 window 对象上,在整个页面加载完成后(包括样式表,图片和其他资源)触发。这个事件可以通过 onload 属性绑定事件

下边的例子正确的显示了图片的尺寸,因为 window.onload 会等待所有图片加载完后触发:

<script>
  window.onload = function() { // same as window.addEventListener('load', (event) => {
    alert('Page loaded');

    // image is loaded at this time
    alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`);
  };
</script>

<img >

window.onunload

当一个访问这离开页面,window 上的 unload 事件会被触发。我们可以做一些不涉及延迟的操作,比如关闭相关的弹出窗口

值得注意的就是发送分析数据

比方说我们收集页用户对页面如何使用的数据:鼠标点击,滚动,查看页面区域等等...也就是说,我们可以在用户离开页面时,通过 unload 事件我们可以向服务器保存数据

这里有一个特殊的方法 navigator.sendBeacon(url, data) 可以满足我们的需求:https://w3c.github.io/beacon/ . 他会在后台发送数据。切换到另一个页面不会延迟:浏览器离开页面后,sendBeacon 仍然会执行,用法如下:

let analyticsData = { /* object with gathered data */ };

window.addEventListener("unload", function() {
  navigator.sendBeacon("/analytics", JSON.stringify(analyticsData));
});
  • 请求方法是 POST
  • 不仅可以发送字符串,也可以发送表单和其他格式的数据,跟 Fetch章节提到的格式一样,通常情况下是一个字符串形式的对象
  • 数据最大限制为64kb

sendBeacon 请求完成时,浏览器可能早已经关闭或者切换了页面,因此没有方法获取服务端的响应

还有一个 keepalive 标志,用于在 fetch方法中为通用的网络请求执行此类 after-page-left 的请求。你可以在 Fetch API 查看详情。

如果我们取消掉(return false)到另一个页面, unload 事件无能为力。但是我们可以使用另一个事件 - onbeforeunload

window.onbeforeunload

当用户准备导航到其他页面或者尝试关掉当前页面, beforeunload 处理程序会要求额外的确认。

如果我们取消事件,浏览器可能会询问用户是否确认离开。

你可以通过尝试执行下边的代码,然后刷新页面:

window.onbeforeunload = function() {
  return false;
};

由于历史原因,返回一个非空的字符串也相当于取消事件。一些古老版本的浏览器曾经将其作为信息(非空字符串)展示,但是现代规范不建议这么做。这是个例子:

window.onbeforeunload = function() {
  return "There are unsaved changes. Leave now?";
};

现代浏览器的应为已经改变,因为曾经一些网站管理员滥用此事件:显示一些误导和烦人的信息给用户显示。因此,现在的话一些老的浏览器仍然会将返回的非空字符串作为消息显示,但是除此之外 - 没有别的可以自定义显示信息给用户的方法

readyState

如果我们在文档加载完成后 绑定 DOMContentLoaded 事件会发生什么? 很显然,永远不会触发。

有时候我们不确定文档是否已经准备好了。我们希望我们的函数在dom加载完执行。

document.readyState 属性会告诉我们当前的加载状态,有3种可能的值:

  • "loading" - 文档正在加载
  • "interactive" - 文档被充分阅读,dom可以访问了
  • "complete" - 文档已经被完全准备就绪,所有资源(比如图片)也加载了

因此,我们可以检查 document.readyState 的值并设置一个处理函数或者立即执行代码当dom准备完毕的时候:

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

if (document.readyState == 'loading') {
  // still loading, wait for the event
  document.addEventListener('DOMContentLoaded', work);
} else {
  // DOM is ready!
  work();
}

当文档加载状态改变的时候,readystatechange 事件会被触发,因此我们可以打印所有的状态:

// current state
console.log(document.readyState);

// print state changes
document.addEventListener('readystatechange', () => console.log(document.readyState));

readystatechange事件是一个跟踪文档加载状态的可选机制,很早就出现了。但是,现在用的很少了

为了完整性,我们看下完整的事件流

这个文档有<iframe>, <img>和处理程序(打印事件)

<script>
  log('initial readyState:' + document.readyState);

  document.addEventListener('readystatechange', () => log('readyState:' + document.readyState));
  document.addEventListener('DOMContentLoaded', () => log('DOMContentLoaded'));

  window.onload = () => log('window onload');
</script>

<iframe src="iframe.html" onload="log('iframe onload')"></iframe>

<img src="http://en.js.cx/clipart/train.gif" >
<script>
  img.onload = () => log('img onload');
</script>

代码示例详情 sandbox

输出如下:

  1. [1] initial readyState:loading
  2. [2] readyState:interactive
  3. [2] DOMContentLoaded
  4. [3] iframe onload
  5. [4] img onload
  6. [4] readyState:complete
  7. [4] window onload

方括号里的数字表示发生时事件相近。标有相同数字的事件大约同时发生(+-几毫秒误差)

  • document.readyStateDOMContentLoaded 之前 变为 interactive。这两件事实际上是一个意思
  • document.readyState 当所有资源(iframe和img)全部加载完 变为 complete。我们可以看到 img.onload(img是最后一个资源)和window.onload同事发生。转变为 complete 状态和 window.onload意味着一个意思。不同之处在于,window.onload 总是在其他 load 事件之后

总结

页面加载事件:

  • 当dom准备就绪,document上的 DOMContentLoaded 被触发。在这个阶段我们可以在dom元素上使用js
    • <script>...</script> 或者 <script src="..."></script> 会阻塞 DOMContentLoaded,浏览器会等待它们执行
    • 图片或者其他资源会继续加载
  • 当页面所有资源加载完成,window上的load 事件被触发。我们很少用它,因为通常不需要等这么久
  • 当用户想离开页面时,window上的beforeunload 事件被触发。如果取消事件,浏览器会询问用户是否要离开页面(比如我们没有保存用户的修改)
  • 当用户最重正在离开,window上的unload事件被触发,在回调中我们只能做一些不涉及延迟或者询问用户的简单事情。因为这个限制,我们很少使用。我们可以使用navigator.sendBeacon来发送网络请求
  • document.readyState 是当前文档的状态,可以通过readystatechange事件来追踪状态的改变:
    • loading - 文档正在加载
    • interactive - 文档已被解析,几乎跟DOMContentLoaded 同时发生,但是会在DOMContentLoaded之前发生
    • complete - 文档所有资源被加载,几乎跟window.onload 同事发生,但是会在DOMContentLoaded之前发生

【原文】:Page: DOMContentLoaded, load, beforeunload, unload