✍️ Tangxt | ⏳ 2020-06-24 | 🏷️ 浏览器底层渲染机制 |
除了 CSS 这样的资源以外,页面还有 JS、图片等这样的资源,其中图片基本跟 CSS 一样,而 JS 则跟 CSS 很不一样……
页面在加载的过程中遇到了这样一段代码:
<!-- 1.js 里边有操作 DOM 的代码 -->
<script src="./1.js"></script>
那么主线程就会从服务器获取到 JS 资源,资源获取完后,就会把 JS 资源进行解析加载 -> 加载完成后再继续渲染 DOM 结构
注意:内嵌 JS 也是会阻塞 DOM 的解析,如
<script>
let btn = document.getElementById('btn')
console.log(btn)
</script>
所以,像这样的 JS 代码是同步的……
在面试中,我们会遇到这样一个问题:
为啥要把 link 写在 head 里边,而 script 则紧挨着 body 结束标签?
因为:
话说,我非得要把 script
标签放在上边呢?即放在 head
标签里边呢?
简而言之,就是如何让 JS 的加载是异步的?
做法很简单,只需要设置 defer
or async
就行了
<script defer src="1.js"></script>
<!-- or -->
<script async src="1.js"></script>
它们俩都是让其变为异步获取资源,即遇到这样有 async
or defer
标识的 script
就会新开一个线程去发请求,所以这并不会阻碍 DOM 的渲染
话说,它们俩是否有区别?如果没有区别的话,为啥还有多搞出一个东西来呢?
defer
可以遵循原有的加载顺序,等所有 JS 资源获取完后会按照顺序去一次渲染 JSasync
是无序的,谁先获取到就先执行谁!何时用 defer
?
加载的 JS 资源互相依赖
何时用 async
?
像类似百度统计这样的 JS 代码,不跟其它 JS 有任何关系的代码,就可以用它!
注意: dfefer
or async
对内嵌 JS 是无效的,遇到内嵌 JS 的 script
标签,一定会阻塞后边的 DOM 渲染的!
<body>
<script async>
console.log(document.getElementById("btn")); // -> null
</script>
<!-- <button id="btn">button</button> -->
<script defer src="./1.js"></script>
<button id="btn">button</button>
</body>
在实际开发中,我们经常会觉得 async
和 defer
没用,其实这很有用呀!只要你打算把 JS 放在上边就会用到……只是我们一般都遵循行业的规范,把 JS 都放在了末尾,所以就没用罢了!
对了,现代浏览器(IE9/10 等)都有完善的代码扫描机制:
如果遇到 scrip 需要同步加载和渲染代码,那么浏览器在渲染 JS 的时候,同时会向下继续扫描代码,如果发现有一些异步的资源代码,就会启动一个新的小线程去加载请求,等 JS 执行完后,可能一些异步资源也被请求回来了……
浏览器为了它的性能,都会做出一些优化的,所以开发者自己不需要考虑这样的情况,如同步加载 JS 阻塞后边的 DOM 渲染,但是后边的 HTML 代码有需要加载异步资源……
还有:
由于在 JS 中还有可能操作元素的样式,所以哪怕都是异步请求资源的情况下,JS 先加载回来,也要等到它之前发送的 CSS 加载并渲染完成后才会执行 JS 代码
说白了,你异步请求回来的 JS 回来了,但是异步请求的 CSS 还没有回来,所以此时的 JS 必须等到 CSS 回来后才会去执行,而不是谁回来了,就谁先执行……
同步的 JS 代码也得等前边异步的 CSS 代码加载渲染后才会执行 JS 代码
注意,如果你这样写: 1.css
-> 2.css
-> 1.js
-> 2.js
-> 3.css
-> 4.css
……
那么异步加载先回来的 2.js
会等 1.css
和 2.css
都加载渲染完后,才会去执行,至于 3.css
和 4.css
就不管了……
它们俩的区别:
script
标签前边的 CSS 都加载渲染完了,然后所有的 JS 也都执行加载了,那么此时才会触发这个事件DOMContentLoaded
被触发之后才触发所以,如果我们非得要在 head 标签里边写 js,那么除了用 defer
or async
获取 DOM 元素以外,还有一种姿势:
<body>
<script src="./2.js"></script>
<button id="btn">button</button>
</body>
// 2.js
window.addEventListener("DOMContentLoaded", () => {
console.log(document.getElementById("btn"));
});
同理,load
事件也行……毕竟load
比DOMContentLoaded
还要晚触发!
在 jQuery 类库中:
$(function(){})
// <=>
$(document).ready(function(){})
当 DOM 结构加载完才会执行函数中的代码 -> 其中原理就是应用了DOMContentLoaded
事件完成的,不过DOMContentLoaded
在低版本浏览器中不兼容(IE6/7) -> 不兼容,那么就使用onreadystatechange
事件,该事件的目的是提供与文档或元素的加载状态有关的信息,在这个事件中监听document.onreadystatechange
值,值为complete
代表 DOM 结构加载完成了
而 load 事件在 jQuery 里边体现是这样的:
$(document).load(function() { // ... 代码。.. });
相较于DOMContentLoaded
是 DOM 2 的事件,而onload
事件则是所有的浏览器都支持,所以我们不需要什么兼容
不过,我们一般不会用load
事件,即便它兼容性好,所以在用 jquery 的时候,我们一般都会将函数调用写在 ready 方法内,就是页面被解析后,我们就可以访问整个页面的所有 dom 元素,可以缩短页面的可交互时间,提高整个页面的体验。
以上这两个 jQuery 代码片段,你都可以到 jQuery 源码里边找到它们俩的实现……注意,其中那个 onreadystatechange
你得在 jQuery1.3 这样的版本中找了!因为最新版的没有它了(除了 ajax 的封装)!
➹:DOMContentLoaded 与 load 的区别 - CaiBoBo - 博客园
➹:Window: DOMContentLoaded event - Web APIs - MDN
defer
和async
有了更深的认识现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有 HTML 解析之前开始构建和布局渲染树。部分的内容将被解析并显示。也就是说浏览器能够渲染不完整的 dom 树和 cssom,尽快的减少白屏的时间。假如我们将 js 放在 header,js 将阻塞解析 dom,dom 的内容会影响到 First Paint,导致 First Paint 延后。所以说我们会将 js 放在后面,以减少 First Paint 的时间,但是不会减少 DOMContentLoaded 被触发的时间
➹:使用 Paint Timing API 提高性能 - 知乎
➹:First Meaningful Paint 首次有效绘制时间 - AlloyTeam
➹:First paint - MDN Web Docs Glossary: Definitions of Web-related terms - MDN