zf-fe

✍️ Tangxt ⏳ 2020-06-24 🏷️ 浏览器底层渲染机制

26-浏览器底层渲染机制 2:有关 JS 的处理

除了 CSS 这样的资源以外,页面还有 JS、图片等这样的资源,其中图片基本跟 CSS 一样,而 JS 则跟 CSS 很不一样……

★有关 JS 的处理

页面在加载的过程中遇到了这样一段代码:

<!-- 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 资源互相依赖

何时用 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>

在实际开发中,我们经常会觉得 asyncdefer 没用,其实这很有用呀!只要你打算把 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.css2.css 都加载渲染完后,才会去执行,至于 3.css4.css 就不管了……

★DOMContentLoaded 事件 和 load 事件

事件触发的执行顺序

它们俩的区别:

所以,如果我们非得要在 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事件也行……毕竟loadDOMContentLoaded还要晚触发!

在 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

★总结

★Q&A

1)First Paint?

现代浏览器为了更好的用户体验,渲染引擎将尝试尽快在屏幕上显示的内容。它不会等到所有 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