浏览器内核
相信大部分的前端同学都是基于谷歌浏览器进行编码,IE的应该是极少数了吧,微软早在几年前就已经表示希望用户不要使用IE游览器尤其是旧版本的,仅仅作为兼容工具使用,因为考虑到一些旧项目需要使用,所以保留在系统内。做过IE兼容性的同学们都知道IE是多么让人头疼 🤦♂️,现在我们经常使用的主流内核大概这几种:
- Chrome浏览器内核:我们都叫chrome内核,以前是Webkit内核,现在是Blink内核
- Firefox浏览器内核:Gecko内核,俗称Firefox内核
- Safari浏览器内核:Webkit内核
浏览器的内核是多线程的,一个浏览器一般至少实现三个常驻线程:
-
javascript引擎:是基于事件驱动单线程执行的,JS引擎一直等待着任务队列中任务的到来,然后加以处理,浏览器无论什么时候都只有一个JS线程在运行JS程序。
-
GUI渲染线程:负责渲染浏览器界面,当界面需要重排、重绘或由于某种操作引发回流时,该线程就会执行。但需要注意 GUI渲染线程与JS引擎是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
-
事件触发线程:当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeOut、也可来自浏览器内核的其他线程如鼠标点击、AJAX异步请求等,但由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
那游览器是如何渲染的呢?从你输入url到页面渲染大致如下步骤:
- DNS解析IP地址
- 建立TCP连接
- 发送http请求
- 关闭TCP连接
- 浏览器渲染(只对本项重点叙述)
渲染引擎(GUI)
游览器渲染流程大致如下:
- GUI将HTML内容转换为DOM树结构。
- GUI将CSS样式表转换为浏览器可解析的stylesheet。
- 建立元素布局信息。
- 在3的基础上建立分层树。
- 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
- 合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上。
总结一下上面的说法就是,首先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在浏览器中的事件循环机制。
-
所有同步任务都在主线程上执行,形成一个执行栈。
-
主线程外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列里放置一个事件(回调)。
-
当执行栈中的同步任务执行完后,系统就会读取任务队列里的事件,那些对应的异步任务结束等待状态,进入执行栈开始执行。
-
主线程不断重复以上步骤。
于是,所有任务可以分成两种,一种是同步任务,另一种是异步任务。
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务指的是,不进入主线程、而进入任务队列的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
任务队列
那在任务队列里存放的各种事件又是怎么个情况?首先他们分为宏任务和微任务。
宏任务 | 微任务 | |
---|---|---|
谁发起的 | 宿主(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);
---------------------------------------------揭晓----------------------------------------------
⚠ 过程分析 ⚠
- 从上至下,先遇到
new Promise
,执行其中的同步代码1 - 再遇到
resolve('success')
, 将promise
的状态改为了resolved
并且将值保存下来 - 继续执行同步代码2
- 跳出
promise
,往下执行,碰到promise.then
这个微任务,将其加入微任务队列 - 执行同步代码4
- 本轮宏任务全部执行完毕,检查微任务队列,发现
promise.then
这个微任务且状态为resolved
,执行它。
其实有很多人会混淆很多概念比方任务队列和微任务队列、甚至同步任务、异步任务与宏任务、微任务混淆到一起,实际在还没有Promise之前,JS是不能发起异步请求的,那个时候只有同步任务。
宏任务、微任务、任务队列(存放事件回调)是由异步任务衍生出来的。
常见问题
Q:DOM树节点和渲染树节点一一对应吗,有什么是DOM树会有,渲染树不会有的节点?
-
DOM 树与 HTML 标签一一对应,包括 head 和隐藏元素。
-
渲染树不包括 head 和隐藏元素,大段文本的每一个行都是独立节点,每一个节点都有对应的 css 属性。
Q:CSS会阻塞dom解析吗?
- 对于一个HTML文档来说,不管是内联还是外链的css,都会阻碍后续的dom渲染,但是不会阻碍后续dom的解析。
Q:重绘和回流(重排)的区别和关系?
-
重绘:当渲染树中的元素外观(如:颜色)发生改变,不影响布局时,产生重绘。
-
回流:当渲染树中的元素的布局(如:尺寸、位置、隐藏/状态状态)发生改变时,产生重绘回流。
-
注意:JS 获取 Layout 属性值(如:offsetLeft、scrollTop、getComputedStyle 等)也会引起回流。因为浏览器需要通过回流计算最新值回流必将引起重绘,而重绘不一定会引起回流
Q:存在阻塞的 CSS 资源时,浏览器会延迟 JavaScript 的执行和 DOM 构建?
-
当浏览器遇到一个 script 标记时,DOM 构建将暂停,直至脚本完成执行。
-
JavaScript 可以查询和修改 DOM 与 CSSOM。
-
CSSOM 构建时,JavaScript 执行将暂停,直至 CSSOM 就绪。
-
所以,script 标签的位置很重要。实际使用时,可以遵循下面两个原则:
- CSS 优先:引入顺序上,CSS 资源先于 JavaScript 资源。
- JavaScript 应尽量少影响 DOM 的构建。
Q:关于CSS加载的阻塞情况?
-
css加载不会阻塞DOM树的解析
-
css加载会阻塞DOM树的渲染
-
css加载会阻塞后面js语句的执行
Q:关键渲染路径详述?
-
浏览器下载html文件。
-
浏览器读取html文件,发现里面包含一张图片、一个css文件和一个js文件。
-
浏览器开始下载图片。
-
浏览器阻塞渲染,直到css和js文件下载完成。
-
浏览器下载css文件并解析,确认没有内嵌的额外资源(通过import)需要记载。
-
浏览器在未下载完js文件前,继续组赛渲染。
-
浏览器下载完js文件并解析,确保没有额外的资源需要加载。
-
最后浏览器渲染出页面。
总结
实际上关于浏览器的渲染引擎和JS引擎还有很多内容可以说,大家有兴趣可以自行去拓展,若有更好的意见或有问题,欢迎随时留言,同时也别忘了点赞关注收藏三连击🤞。