👣探索浏览器的秘密👣

584 阅读8分钟

浏览器内核

相信大部分的前端同学都是基于谷歌浏览器进行编码,IE的应该是极少数了吧,微软早在几年前就已经表示希望用户不要使用IE游览器尤其是旧版本的,仅仅作为兼容工具使用,因为考虑到一些旧项目需要使用,所以保留在系统内。做过IE兼容性的同学们都知道IE是多么让人头疼 🤦‍♂️,现在我们经常使用的主流内核大概这几种:

  1. Chrome浏览器内核:我们都叫chrome内核,以前是Webkit内核,现在是Blink内核
  2. Firefox浏览器内核:Gecko内核,俗称Firefox内核
  3. Safari浏览器内核:Webkit内核

浏览器的内核是多线程的,一个浏览器一般至少实现三个常驻线程:

  1. javascript引擎:是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。

  2. GUI渲染线程:负责渲染浏览器界面,当界面需要重排、重绘或由于某种操作引发回流时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。

  3. 事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。

那游览器是如何渲染的呢?从你输入url到页面渲染大致如下步骤:

  1. DNS解析IP地址
  2. 建立TCP连接
  3. 发送http请求
  4. 关闭TCP连接
  5. 浏览器渲染(只对本项重点叙述)

渲染引擎(GUI)

游览器渲染流程大致如下:

  1. GUI将HTML内容转换为DOM树结构。
  2. GUI将CSS样式表转换为浏览器可解析的stylesheet。
  3. 建立元素布局信息。
  4. 在3的基础上建立分层树。
  5. 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
  6. 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。

总结一下上面的说法就是,首先GUI基于HTML生成一个DOM树,然后与解析处理的CSS树进行结合形成渲染树(render),然后基于render为蓝图计算布局和绘图,页面的初次渲染就完成了。

之后每当一个新元素加入到这个 DOM 树当中,浏览器便会通过 CSS 引擎查遍 CSS 样式表,找到符合该元素的样式规则应用到这个元素上,然后再重新去绘制它。

在远古时期时,那时候jq还很流行,将各种DOM的操作都封装到一个库里调简单的api即可使用,称霸了那时的前端,实际上DOM操作对于页面的性能开销是非常大的,因为每次DOM操作之后浏览器都会重绘,改变布局了会回流,而vue和react的出现也是缓解了这一问题,通过diff算法比对新旧DOM树去进行更新。

JS引擎

JS引擎组成

  • 编译器。主要工作是将源代码编译成抽象语法树,然后在某些引擎中还包含将抽象语法树转换成字节码。

  • 解释器。在某些引擎中,解释器主要是接受字节码,解释执行这个字节码,然后也依赖来及回收机制等。

  • JIT工具。一个能够JIT的工具,将字节码或者抽象语法树转换成本地代码。

  • 垃圾回收器和分析工具(profiler)。它们负责垃圾回收和收集引擎中的信息,帮助改善引擎的性能和功效。

JS事件循环(event loop)与 事件队列

同步与异步

说到浏览器的JS执行就不得不说到JS在浏览器中的事件循环机制。

  1. 所有同步任务都在主线程上执行,形成一个执行栈。

  2. 主线程外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列里放置一个事件(回调)。

  3. 当执行栈中的同步任务执行完后,系统就会读取任务队列里的事件,那些对应的异步任务结束等待状态,进入执行栈开始执行。

  4. 主线程不断重复以上步骤。

于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。

异步任务指的是,不进入主线程、而进入任务队列的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

任务队列

那在任务队列里存放的各种事件又是怎么个情况?首先他们分为宏任务和微任务。

宏任务微任务
谁发起的宿主(Node、浏览器)JS引擎
具体事件script (可以理解为外层同步代码)、setTimeout/setInterval、UI rendering/UI事件、postMessage,MessageChannel、setImmediate,I/O(Node.js)Promise.then、MutaionObserver
谁先运行后运行先运行
会触发新一轮Tick吗不会

来一道简单的题目理解一下:

const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
  console.log(2);
});
promise.then(() => {
  console.log(3);
});
console.log(4);

---------------------------------------------揭晓----------------------------------------------

⚠ 过程分析 ⚠

  1. 从上至下,先遇到new Promise,执行其中的同步代码1
  2. 再遇到resolve('success'), 将promise的状态改为了resolved并且将值保存下来
  3. 继续执行同步代码2
  4. 跳出promise,往下执行,碰到promise.then这个微任务,将其加入微任务队列
  5. 执行同步代码4
  6. 本轮宏任务全部执行完毕,检查微任务队列,发现promise.then这个微任务且状态为resolved,执行它。

其实有很多人会混淆很多概念比方任务队列和微任务队列、甚至同步任务、异步任务与宏任务、微任务混淆到一起,实际在还没有Promise之前,JS是不能发起异步请求的,那个时候只有同步任务。

宏任务、微任务、任务队列(存放事件回调)是由异步任务衍生出来的。

常见问题

Q:DOM树节点和渲染树节点一一对应吗,有什么是DOM树会有,渲染树不会有的节点?

  1. DOM 树与 HTML 标签一一对应,包括 head 和隐藏元素。

  2. 渲染树不包括 head 和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的 css 属性。

Q:CSS会阻塞dom解析吗?

  1. 对于一个HTML文档来说,不管是内联还是外链的css,都会阻碍后续的dom渲染,但是不会阻碍后续dom的解析。

Q:重绘和回流(重排)的区别和关系?

  1. 重绘:当渲染树中的元素外观(如:颜色)发生改变,不影响布局时,产生重绘。

  2. 回流:当渲染树中的元素的布局(如:尺寸、位置、隐藏/状态状态)发生改变时,产生重绘回流。

  3. 注意:JS 获取 Layout 属性值(如:offsetLeft、scrollTop、getComputedStyle 等)也会引起回流。因为浏览器需要通过回流计算最新值回流必将引起重绘,而重绘不一定会引起回流

Q:存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建?

  1. 当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。

  2. JavaScript 可以查询和修改 DOM 与 CSSOM。

  3. CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。

  4. 所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:

  • CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
  • JavaScript 应尽量少影响 DOM 的构建。

Q:关于CSS加载的阻塞情况?

  1. css加载不会阻塞DOM树的解析

  2. css加载会阻塞DOM树的渲染

  3. css加载会阻塞后面js语句的执行

Q:关键渲染路径详述?

  1. 浏览器下载html文件。

  2. 浏览器读取html文件,发现里面包含一张图片、一个css文件和一个js文件。

  3. 浏览器开始下载图片。

  4. 浏览器阻塞渲染,直到css和js文件下载完成。

  5. 浏览器下载css文件并解析,确认没有内嵌的额外资源(通过import)需要记载。

  6. 浏览器在未下载完js文件前,继续组赛渲染。

  7. 浏览器下载完js文件并解析,确保没有额外的资源需要加载。

  8. 最后浏览器渲染出页面。

总结

实际上关于浏览器的渲染引擎和JS引擎还有很多内容可以说,大家有兴趣可以自行去拓展,若有更好的意见或有问题,欢迎随时留言,同时也别忘了点赞关注收藏三连击🤞。