页面生命周期:DOMContentLoaded, load, beforeunload, unload解析
HTML页面的生命周期有以下三个重要事件:
-
DOMContentLoaded
—— 浏览器已经完全加载了 HTML,DOM 树已经构建完毕,但是像是<img>
和样式表等外部资源可能并没有下载完毕。 -
load
—— 浏览器已经加载了所有的资源(图像,样式表等)。 -
beforeunload/unload
—— 当用户离开页面的时候触发。
每个事件都有特定的用途
-
DOMContentLoaded
—— DOM 加载完毕,所以 JS 可以访问所有 DOM 节点,初始化界面。 -
load
—— 附加资源已经加载完毕,可以在此事件触发时获得图像的大小(如果没有被在 HTML/CSS 中指定) -
beforeunload/unload
—— 用户正在离开页面:可以询问用户是否保存了更改以及是否确定要离开页面。
来看一下每个事件的细节。
DOMContentLoaded
DOMContentLoaded
由 document
对象触发。
我们使用 addEventListener
来监听它:
document.addEventListener("DOMContentLoaded", ready);
举个例子
<script> function ready() { alert('DOM is ready'); // image is not yet loaded (unless was cached), so the size is 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">
在这个例子中 DOMContentLoaded
在 document 加载完成后就被触发,无需等待其他资源的载入,所以 alert
输出的图像的大小为 0。
这么看来 DOMContentLoaded
似乎很简单,DOM 树构建完毕之后就运行该事件,不过其实存在一些陷阱。
DOMContentLoaded 和脚本
当浏览器在解析 HTML 页面时遇到了 <script>...</script>
标签,将无法继续构建DOM树(译注:UI 渲染线程与 JS 引擎是互斥的,当 JS 引擎执行时 UI 线程会被挂起),必须立即执行脚本。所以 DOMContentLoaded
有可能在所有脚本执行完毕后触发。
外部脚本(带 src
的)的加载和解析也会暂停DOM树构建,所以 DOMContentLoaded
也会等待外部脚本。
不过有两个例外是带 async
和 defer
的外部脚本,他们告诉浏览器继续解析而不需要等待脚本的执行,所以用户可以在脚本加载完成前可以看到页面,有较好的用户体验。
async
和 defer
属性仅仅对外部脚本起作用,并且他们在 src
不存在时会被自动忽略。
它们都告诉浏览器继续处理页面上的内容,而在后台加载脚本,然后在脚本加载完毕后再执行。所以脚本不会阻塞DOM树的构建和页面的渲染。
(译注:带有 async
和 defer
的脚本的下载是和 HTML 的下载与解析是并行的,但是 JS 的执行一定是和 UI 线程是互斥的,像下面这张图所示,async
在下载完毕后的执行会阻塞HTML的解析)
他们有两处不同:
async |
defer |
|
---|---|---|
顺序 | 带有 async 的脚本是优先执行先加载完的脚本,他们在页面中的顺序并不影响他们执行的顺序。 |
带有 defer 的脚本按照他们在页面中出现的顺序依次执行。 |
DOMContentLoaded |
带有 async 的脚本也许会在页面没有完全下载完之前就加载,这种情况会在脚本很小或本缓存,并且页面很大的情况下发生。 |
带有 defer 的脚本会在页面加载和解析完毕后执行,刚好在 DOMContentLoaded 之前执行。 |
所以 async
用在那些完全不依赖其他脚本的脚本上。
DOMContentLoaded 与样式表
外部样式表并不会阻塞 DOM 的解析,所以 DOMContentLoaded
并不会被它们影响。
不过仍然有一个陷阱:如果在样式后面有一个内联脚本,那么脚本必须等待样式先加载完。
(译注:简单来说,JS 因为有可能会去获取 DOM 的样式,所以 JS 会等待样式表加载完毕,而 JS 是阻塞 DOM 的解析的,所以在有外部样式表的时候,JS 会一直阻塞到外部样式表下载完毕)
<link type="text/css" rel="stylesheet" href="style.css"> <script> // 脚本直到样式表加载完毕后才会执行。 alert(getComputedStyle(document.body).marginTop); </script>
发生这种事的原因是脚本也许会像上面的例子中所示,去得到一些元素的坐标或者基于样式的属性。所以他们自然要等到样式加载完毕才可以执行。
DOMContentLoaded
需要等待脚本的执行,脚本又需要等待样式的加载。
浏览器的自动补全
Firefox, Chrome 和 Opera 会在 DOMContentLoaded
执行时自动补全表单。
例如,如果页面有登录的界面,浏览器记住了该页面的用户名和密码,那么在 DOMContentLoaded
运行的时候浏览器会试图自动补全表单(如果用户设置允许)。
所以如果 DOMContentLoaded
被一个需要长时间执行的脚本阻塞,那么自动补全也会等待。你也许见过某些网站(如果你的浏览器开启了自动补全)—— 浏览器并不会立刻补全登录项,而是等到整个页面加载完毕后才填充。这就是因为在等待 DOMContentLoaded
事件。
使用带 async
和 defer
的脚本的一个好处就是,他们不会阻塞 DOMContentLoaded
和浏览器自动补全。(译注:其实执行还是会阻塞的)
2018.02.05:defer
是会阻塞 DOMContentLoaded
的,被 defer
的脚本要在 DOMContentLoaded
触发前执行,所以如果HTML很快就加载完了(先不考虑 CSS 阻塞 DOMContentLoaded
的情况),而 defer
的脚本还没有加载完,浏览器就会等,等到脚本加载完,执行完,再触发 DOMContentLoaded
,放上一张图(取自在 devTool 下分析自己写的一个页面)
可以看到,HTML很快就加载和解析完毕(CSS 在这里是动态加载的,不阻塞 DOMContentLoaded
),jQuery 和main.js 的脚本是 defer
的, DOMContentLoaded
(蓝线)一直在等,等到这两个脚本下载完并执行完,才触发了 DOMContentLoaded
。
从这个角度看来,defer
和把脚本放在 </body>
前真是没啥区别,只不过 defer
脚本位 于head
中,更早被读到,加载更早,而且不担心会被其他的脚本推迟下载开始的时间。
window.onload
window
对象上的 onload
事件在所有文件包括样式表,图片和其他资源下载完毕后触发。
下面的例子正确检测了图片的大小,因为 window.onload
会等待所有图片的加载。
<script> window.onload = function() { alert('Page loaded'); // image is loaded at this time alert(`Image size: ${img.offsetWidth}x${img.offsetHeight}`); }; </script> <img id="img" src="https://en.js.cx/clipart/train.gif?speed=1&cache=0">
window.onunload
用户离开页面的时候,window
对象上的 unload
事件会被触发,我们可以做一些不存在延迟的事情,比如关闭弹出的窗口,可是我们无法阻止用户转移到另一个页面上。
所以我们需要使用另一个事件 — onbeforeunload
。
window.onbeforeunload
如果用户即将离开页面或者关闭窗口时,beforeunload
事件将会被触发以进行额外的确认。
浏览器将显示返回的字符串,举个例子:
window.onbeforeunload = function() { return "There are unsaved changes. Leave now?"; };
有些浏览器像 Chrome 和火狐会忽略返回的字符串取而代之显示浏览器自身的文本,这是为了安全考虑,来保证用户不受到错误信息的误导。
readyState
如果我们在整个页面加载完毕后设置 DOMContentLoaded
会发生什么呢?
啥也没有,DOMContentLoaded
不会被触发。
有一些情况我们无法确定页面上是否已经加载完毕,比如一个带有 async
的外部脚本的加载和执行是异步的**(译注:执行不是异步的 -_-)**。在不同的网络状况下,脚本有可能是在页面加载完毕后执行也有可能是在页面加载完毕前执行,我们无法确定。所以我们需要知道页面加载的状况。
document.readyState
属性给了我们加载的信息,有三个可能的值:
-
loading
加载 —— document仍在加载。 -
interactive
互动 —— 文档已经完成加载,文档已被解析,但是诸如图像,样式表和框架之类的子资源仍在加载。 -
complete
—— 文档和所有子资源已完成加载。状态表示load
事件即将被触发。
所以我们可以检查 document.readyState
的状态,如果没有就绪可以选择挂载事件,如果已经就绪了就可以直接立即执行。
像这样:
function work() { /*...*/ } if (document.readyState == 'loading') { document.addEventListener('DOMContentLoaded', work); } else { work(); }
每当文档的加载状态改变的时候就有一个 readystatechange
事件被触发,所以我们可以打印所有的状态。
// current state console.log(document.readyState); // print state changes document.addEventListener('readystatechange', () => console.log(document.readyState));
readystatechange
是追踪页面加载的一个可选的方法,很早之前就已经出现了。不过现在很少被使用了,为了保持完整性还是介绍一下它。
readystatechange
的在各个事件中的执行顺序又是如何呢?
<script> function log(text) { /* output the time and message */ } 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" οnlοad="log('iframe onload')"></iframe> <img src="http://en.js.cx/clipart/train.gif" id="img"> <script> img.onload = () => log('img onload'); </script>
输出如下:
[1] initial readyState:loading
[2] readyState:interactive
[2] DOMContentLoaded
[3] iframe onload
[4] readyState:complete
[4] img onload
[4] window onload
方括号中的数字表示他们发生的时间,真实的发生时间会更晚一点,不过相同数字的时间可以认为是在同一时刻被按顺序触发(误差在几毫秒之内)
-
document.readyState
在DOMContentLoaded
前一刻变为interactive
,这两个事件可以认为是同时发生。 -
document.readyState
在所有资源加载完毕后(包括iframe
和img
)变成complete
,我们可以看到complete
、img.onload
和window.onload
几乎同时发生,区别就是window.onload
在所有其他的load
事件之后执行。
总结
页面事件的生命周期:
-
DOMContentLoaded
事件在DOM树构建完毕后被触发,我们可以在这个阶段使用 JS 去访问元素。-
async
和defer
的脚本可能还没有执行。 - 图片及其他资源文件可能还在下载中。
-
-
load
事件在页面所有资源被加载完毕后触发,通常我们不会用到这个事件,因为我们不需要等那么久。 -
beforeunload
在用户即将离开页面时触发,它返回一个字符串,浏览器会向用户展示并询问这个字符串以确定是否离开。 -
unload
在用户已经离开时触发,我们在这个阶段仅可以做一些没有延迟的操作,由于种种限制,很少被使用。 -
document.readyState
表征页面的加载状态,可以在readystatechange
中追踪页面的变化状态:-
loading
—— 页面正在加载中。 -
interactive
—— 页面解析完毕,时间上和DOMContentLoaded
同时发生,不过顺序在它之前。 -
complete
—— 页面上的资源都已加载完毕,时间上和window.onload
同时发生,不过顺序在他之前。
-