- domLoading 表示开始解析第一批收到的 HTML 文档的字节
- domInteractive 表示完成全部 HTML 的解析并且 DOM 构建完毕
- domContentLoaded 表示 DOM 与 CSSOM 皆已准备就绪
- domComplete 表示所有的处理都已完成并且所有的附属资源都已经下载完毕
- loadEvent 作为网页加载的最后一步以便触发附加的应用逻辑
HTML页面的生命周期有以下三个重要事件:
DOMContentLoaded
—— 浏览器已经完全加载了 HTML,DOM 树已经构建完毕,但是像是<img>
和样式表等外部资源可能并没有下载完毕。load
—— 浏览器已经加载了所有的资源(图像,样式表等)。beforeunload/unload
—— 当用户离开页面的时候触发。
每个事件都有特定的用途
DOMContentLoaded
—— DOM 加载完毕,所以 JS 可以访问所有 DOM 节点,初始化界面。load
—— 附加资源已经加载完毕,可以在此事件触发时获得图像的大小(如果没有被在 HTML/CSS 中指定)beforeunload/unload
—— 用户正在离开页面:可以询问用户是否保存了更改以及是否确定要离开页面。
DOMContentLoaded
document.addEventListener("DOMContentLoaded", ready);
img的载入成功与否不会影响
<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
会发生什么呢?
啥也没有,DOMContentLoaded
不会被触发。
所以在vue的mounted里设置该事件的监听并不会发生作用;
DOMContentLoaded等价于 vue的mounted事件, DOM 加载完毕,所以 JS 可以访问所有 DOM 节点
DOMContentLoaded与JS
DOMContentLoaded 事件陷阱:
JS默认是会阻塞DOM和渲染树的构建的。HTML解析器在遇到脚本文件时,默认为停下来去获取脚本(不考虑资源预加载优化),然后执行,期间阻塞DOM构建。(UI 渲染线程与 JS 引擎是互斥的,当 JS 引擎执行时 UI 线程会被挂起)
即 DOMContentLoaded
会等待脚本的加载和执行。
两个例外是带 async
和 defer
的外部脚本
defer:声明为defer的脚本会延迟到DOM构建完成后(DOMInteractive事件),DOMContentLoaded和window.onload事件之前执行(但依然会被浏览器的预加载策略提前下载)
async:声明为异步的脚本会异步地下载和执行,会在window.onload事件之前执行,可能会在DOMContenLoaded事件前执行。
他们有两处不同:
async |
defer |
|
---|---|---|
顺序 | 带有 async 的脚本是优先执行先加载完的脚本,他们在页面中的顺序并不影响他们执行的顺序。 |
带有 defer 的脚本按照他们在页面中出现的顺序依次执行。执行顺序即文档顺序 |
DOMContentLoaded |
带有 async 的脚本也许会在页面没有完全下载完之前就加载,这种情况会在脚本很小或本缓存,并且页面很大的情况下发生。执行时机在load事件派发之前 |
带有 defer 的脚本会在页面加载和解析完毕后执行,刚好在 DOMContentLoaded 之前执行。 |
加载 | 都是并行加载(得益于浏览器的资源预加载) |
所以 async
用在那些完全不依赖其他脚本的脚本上。
结论: async 和 defer都会影响DOMContentLoaded事件,使用defer也会阻碍DOMContentLoaded触发,使用async有可能会在DOMContentLoaded之前加载执行从而阻碍DOMContentLoaded事件,取决于html的加载完之前脚本是否请求到位(一般是脚本很小或本缓存,并且页面很大的情况下发生)。
defer
和把脚本放在 </body>
前真是没啥区别,只不过 defer
脚本位 于head
中,更早被读到,加载更早,而且不担心会被其他的脚本推迟下载开始的时间。
区别:
扩展:
为啥一般将css文件放在头部,js文件放在body里
===> js会影响页面加载吗,样式表会影响页面加载吗? 解析见这里;
资源的优先级:当我们网页上有许多图片,脚本和样式资源时,它们的加载顺序是怎样的呢
===> 样式和脚本的优先级是比图片更高的,因为这两个都具有阻塞性,浏览器让它们尽快下载下来。第一个图片资源会和css一起下载,但浏览器发现后面还有许多图片就会优先去下载脚本文件。
<body>
<img src="/images/img1.png"/>
<img src="/images/img2.png"/>
<img src="/images/img3.png"/>
<img src="/images/img4.png"/>
<script type="text/javascript" src="/js/script1.js"></script>
<script type="text/javascript" src="/js/script2.js"></script>
<script type="text/javascript" src="/js/script3.js"></script>
<p>conten after script1</p>
</body>
DOMContentLoaded与CSS
外部样式表并不会阻塞 DOM 的解析,所以 DOMContentLoaded
并不会被它们影响。
CSS会阻塞渲染树的构建,不阻塞DOM构建,但是在CSSOM构建完成之前,页面不会开始渲染(一片空白),与js不一样,js虽然会阻塞后续DOM构建,但是前面已经就绪的内容会进行渲染。
不过仍然有一个陷阱:CSS虽然不阻塞DOM构建,但是会阻塞后面js的执行,从而间接阻塞完整DOM的构建。即如果在样式后面有一个内联脚本,那么脚本必须等待样式先加载完。由于脚本被css阻塞,而脚本的阻塞会影响dom的构建,从而导致css的解析影响了页面的DOMContentLoaded事件。
(css为啥可以阻塞js? 因为js有可能会去获取 DOM 的样式,所以会等待样式表加载完毕
所以在有外部样式表的时候,JS 会一直被阻塞直到外部样式表下载完毕
**
一般情况下 样式表不会阻塞DOMContentLoaded事件,除非在样式表后引入了js。
但是出于页面的文件资源的最佳放置顺序考虑,一般css放在头部,js放在body前可知,大部分时候css的加载时长是会影响页面的加载。
针对性优化
- 尽早加载css文件并生成CSSOM.
- 不使用@import
- 利用媒体查询,处理特定设备下的CSS,以减少不必要的阻塞.
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 事件会被触发,我们可以做一些不存在延迟的事情,比如关闭弹出的窗口,可是我们无法阻止用户转移到另一个页面上。
window.onbeforeunload
如果用户即将离开页面或者关闭窗口时,beforeunload 事件将会被触发以进行额外的确认。
readyState
确定页面上是否已经加载完毕
document.readyState
属性给了我们加载的信息,有三个可能的值:
loading
加载 —— document仍在加载。interactive
互动 —— 文档已经完成加载,文档已被解析,但是诸如图像,样式表和框架之类的子资源仍在加载。complete
—— 文档和所有子资源已完成加载。状态表示load
事件即将被触发。
function work() { /*...*/ }
if (document.readyState == 'loading') {
document.addEventListener('DOMContentLoaded', work);
} else {
work();
}
检查 document.readyState
的状态,如果没有就绪可以选择挂载事件,如果已经就绪了就可以直接立即执行。
每当文档的加载状态改变的时候就有一个 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" onload="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
在所有资源加载完毕后(包括 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
同时发生,不过顺序在他之前。
扩展:vue的生命周期
横向对比一下vue的生命周期
父组件和子组件生命周期钩子函数执行顺序? data在什么阶段初始化完成? 在什么阶段才能访问操作DOM?
回答:如果想挂载一个loading 应该在哪两个生命周期里写
beforecreated:可以在这加个loading事件
created() : 可以访问到data的数据,但是this.$el还是不存在的;
在这结束loading,还做一些初始化,实现函数自执行
<———created to beforeMount 会触发 compile 虚拟树 到 $el上,但是$el此时还是为虚拟的节点———->
beforeMount(): data、el(虚拟)
mounted():
data、el都是真实的、 在这发起axios请求,拿回数据,配合路由钩子做一些事情
updated():
当组件内响应式数据发生变更
- 初始化组件时,仅执行了beforeCreate/Created/beforeMount/mounted四个钩子函数
- 当改变data中定义的变量(响应式变量)时,会执行beforeUpdate/updated钩子函数
- 当切换组件(当前组件未缓存)时,会执行beforeDestory/destroyed钩子函数
- 初始化和销毁时的生命钩子函数均只会执行一次,beforeUpdate/updated可多次执行
扩展:async与defer
常规script标签:浏览器解析 HTML 文档,解析到没有 defer 或 async 属性的 script 标签时,会阻塞 HTML 文档解析,并按照它们出现的顺序执行;
==> 所以:将需要 DOM 操作的 js 要放在 body 标签的最后,原因就是不阻塞 HTML 文档的解析,使页面尽可能快的在浏览器上渲染呈现。
async属性:加载和渲染后续文档元素的过程将和脚本的加载并行进行,脚本的加载完成后就马上执行,脚本执行时会阻塞 HTML 解析。文件独立,各async文件先加载完的先执行;
总结:下载时与html解析同步,不阻塞,但下载完成后立即执行脚本,会造成阻塞
默认情况下,动态脚本表现为“async”行为。
let script = document.createElement('script');
script.src = "/article/script-async-defer/long.js";
document.body.append(script); // (*)
**
可以通过将 async
属性显示修改为 false
以将加载优先顺序修改为文档顺序
script.async = false;
defer属性:下载与解析并行,但是执行是在dom解析完成之后,DOMContentLoaded 事件触发之前完成。会按照出现顺序执行
扩展:资源onload&onerror
resource.onload 发生在资源loaded且执行之后
img、js都可以添加onload事件
注意:
大部分的资源都是在他们被添加到document事件后开始loading,但是img标签当他有src属性就开始loading
实战:
因为img标签当他们创建时候就被loaded,所以当我们将img插入页面时用户可能看不见img图像,因为此时img被创建了但是并没有获得资源;
为了用户体验,我们可以先加载图像,当引擎执行到将img插入页面时的代码时,可以利用浏览器的记忆功能读取我们预加载的图像资源,使得img可见;
可以实现预加载的原因:img是当遇见src就开始加载 而不是当loaded才加载,所以我们可以先加载 load的时候再用
即写一个img加载完的异步函数 (#图片异步加载)
扩展:页面文件资源的放置顺序
CSS不阻塞dom的生成。
CSS不阻塞js的加载,但是会阻塞js的执行。
js会阻塞dom的生成,也就是会阻塞页面的渲染,那么css也有可能会阻塞页面的渲染。
如果把CSS放在文档的最后面加载执行,CSS不会阻塞DOM的生成,也不会阻塞JS,但是浏览器在解析完DOM后,要花费额外时间来解析CSS,而不是在解析DOM的时候,并行解析CSS。
并且浏览器会先渲染出一个没有样式的页面,等CSS加载完后会再渲染成一个有样式的页面,页面会出现明显的闪动的现象。
所以应该把CSS放在文档的头部,尽可能的提前加载CSS;把JS放在文档的尾部,这样JS也不会阻塞页面的渲染。CSS会和JS并行解析,CSS解析也尽可能的不去阻塞JS的执行,从而使页面尽快的渲染完成。