为什么要深入浏览器?学习它有什么用?
答:规范我们写代码,保证 link
写在 head
内部,操作dom的 script
写在body底部。为什么这么写?我们去了解一下浏览器渲染一个网页的步骤,以及如果不这么写会有什么后果
目录
1. 浏览器基本构造
2. 浏览器如何加载、渲染网页
3. v8
4. event loop
5. 参考文章
关键词:需要对照浏览器、v8源码一起来看看具体实现
这里总结一下 chrome 浏览器多进程架构
进程:独立的运行程序;作为计算机分配资源的单位;进程间通信叫做 ipc 通信
线程:作为进程的执行单位,也就是计算机操作系统调用的单位,用于处理具体任务;一个进程有一个或多个线程
通过 chrome 浏览器的设置 -> 更多工具 -> 任务管理器,可以看到同操作系统提供的任务管理器一样,列出了进程树,不过这里展示的是浏览器相关进程
浏览器内核:渲染引擎、js引擎
trident: ie, 360兼容, 搜狗
gecko: firefox
webkit: safari, chrome老版
blink: chrome opera
blink 属于 webkit 的一个分支
chromium 是 google 更新最快的浏览器,和 chrome 的关系,就像 @babel/preset-env 与 babel-preset-env 一样
在 chrome 中按 f12
可以调出开发者工具,可以看到顶部9个具名模块、图标模块
图标模块:主要用于辅助控制页面,包含选取元素、模拟运行环境(pc、手机、平板) …
9个具名模块
cpu、gpu 作为浏览器的2大核心
gpu: 图形处理单元,用于计算机 图形处理
,而 cpu 多用于常规计算处理。
gpu 加速计算:同时利用 gpu 和 cpu ,将计算密集部分放在 gpu 运行,cpu 运行其余代码。
gpu 图形处理器,显卡的处理器,显卡的核心部分。用于绘制图形,我们通过显示器看到的东西都是 gpu 绘制出来的。浏览器通过渲染引擎调用 gpu 执行渲染任务。浏览器主进程读取 gpu 缓存图片并显示图片。
syn
-> syn + ack
-> ack + data
问2:渲染引擎对于 html 文档及 css, img, js 之间的解析执行顺序是怎么样的?
script
引入 js 时,会阻塞文档的解析,直到资源被请求到并且js被执行(所以都建议将script写在文末,保证html的解析优先)。可以设置 defer
属性,表示该资源在html解析过程中并行异步获取资源,在html解析完成之后才执行js。关于 defer
和 async
的区别可见 深入html-标签语义化外链css 不会阻塞 dom 解析,所以会照样生成 dom tree,但是会阻塞生成 render tree,浏览器会在所有样式表生成样式表对象之后,再来绘制 render tree 外链css 会阻塞 js 的执行,因为js需要获取节点的 css 信息,必须要等 render tree 生成之后,才能执行js
内联css
、缓存中的外链css
不会阻塞 js 的执行,因为不存在网络延迟,不会发起http请求
html5规范规定:
如何加深理解?模拟一次 tls 4次握手
clientHello
、tls版本
、可用的加密算法集合
、可用的压缩算法
、第一个随机数
serverHello
、采用的tls版本
、采用的加密算法
、采用的压缩算法
、ca证书,含公钥
、第二个随机数
确认加密结束
、第三个随机数
、ack
确认加密
、ack
相比与 tcp 的3次握手, tls 的4次握手,就是多了
tls版本
、加密算法
、压缩算法
、ca证书及公钥
、随机数
,每次会话都需要进行 tls 4次握手,所以 http2 采用了长链接 随机数作用:公钥只是用来加密,非对称加密;随机数用于每次会话生成对称加密;第三个随机数会用第二次握手服务器提供的公钥进行加密
工作过程:4次握手结束之后,然后就普通 http 通信了,不过数据都才用会话密钥进行加密传输。这里的会话密钥就是通过那3个随机数、采用的加密算法生成的。比如 RSA密钥交换算法 + 3个随机数 = 特定对称密钥
公钥作用:服务器提供的公钥,用于第三个随机数加密
私钥作用:服务器解密第三个随机数
对称密钥作用:握手之后的 http 通信数据加密
对称秘钥加密解密快,用于真实数据;非对称秘钥加密解密慢,只用于第三个随机数的加密解密。
渲染阶段包含了如下子阶段,每个子阶段都有输入输出,页面渲染也是一个多阶段流水线过程
渲染引擎解析文档 html 结构,生成 dom 对象树
styleSheets
ComputedStyle
属性中输入:dom tree
输出:包含节点样式对象属性的 dom tree
如果是 recalculate 计算,比如回流和重绘,还通常伴有 hit test (计算点击元素最小触发节点算法)、触发 animation frame 事件等子阶段
输入:dom tree
输出:包含几何坐标信息的 render tree
在 render tree 计算完 dom 的几何位置之后,需要计算各节点所处页面分层情况。这里需要知道,浏览器绘制显示页面,同 ps 一样,也是有一个图层效果,可以通过浏览器 layers
来查看具体 3d 图层效果
那么,浏览器是怎么规划图层的呢?我们定义 css 样式,哪些会新建一个图层,哪些会应用在父图层上?图层多少有什么影响?
渲染引擎在生成包含 dom 几何位置的 render tree 之后,就会根据节点的 css 样式,构建一颗 layer tree。layer tree 相较于 render tree,就是每个 dom 节点包含了所属图层信息
输入:render tree
输出:layer tree
dom 新图层成立条件:节点拥有层叠上下文属性,包括节点带有三维效果的样式属性、展示部分内容的属性(裁剪、滚动)
position: absolute | relatice 且 z-index 不为 auto |
输入:layer tree
输出:绘制指令队列
在分层处理之后,生成了一颗 layer tree,那接下来就是生成绘制指令队列。
渲染引擎遍历 layer tree,每一个图层都将对应多条绘制指令,这个取决于图层的复杂度。
每一条绘制指令就是执行一次简单的绘制操作
如图所示,Profiler 就是指令队列,每一条指令是一次简单的 paint 操作,比如绘制底色、矩形
问题:浏览器执行了分层,生成 layer tree,然后又生成绘制指令队列,分层数量多少有什么影响?
合成及生成绘制指令都是在渲染进程的主线程中实现,而实例处理绘制指令队列的是 合成线程
。
输入:包含绘制指令队列的 layer tree
输出:位图
合成线程:将收到的 layer tree 划分为图块(一个图层包含多个图块,每一个图块由多个绘制指令构成),每次只将浏览器 viewport 中的图块交给栅格化线程
栅格化线程:输入图块,然后与 gpu 进程进行 ipc 通信,将图块包含的绘制指令递交给 gpu,gpu 生成图片并保存在自身缓冲区中(gpu raster)
光栅化:也就是将图块转换成图片的过程
待合成线程所有图块光栅化结束,合成线程同浏览器主进程通信,通知主进程去读取 gpu 缓冲区内容,通过像素点展示在显示器屏幕上。
v8 组件
执行上下文
环境对象,主要处理变量提升、创建作用域链、this对象指向调用栈:保存各作用域的执行上下文对象。全局执行上下文环境对象会被压入栈底,遇到函数或其他作用域,会创建新的执行上下文对象,并压入调用栈。
具体阶段流程:
Ignition
,解释 ast 语法树生成字节码文件。字节码是一种特别结构的代码,与机器无关,不能被机器直接执行。turbofan
将这段代码编译成一段机器码缓存起来,下次解释器处理字节码遇到时,直接读取这段机器码解释器 Ignition
执行字节码文件,属于模拟计算机 cpu 执行二进制文件,解释器直接输出计算结果。
解释器本身也是运算在 cpu 之上,只不过不是 cpu 直接处理各种原逻辑,而是由解释器调整过后的逻辑让 cpu 来处理,即在 cpu 之上封装了一层来处理字节码文件。
明显可以看出来,cpu 直接计算速度更快,那么为什么还要使用解释器包一层呢?即解释器和编译器各有啥优点?
解释器:启动速度快,执行速度慢。解释器的存在,是集中处理计算,是虚拟机的组成部分,为了优化自身一些语言设计特点,最终只输出很少的二进制代码交给 cpu 执行。
编译器:启动速度慢,执行速度快
问题:生成 ast 语法树,是会把整个 js 代码都处理了吗?
猜想:会,因为解释器是不能分析判断源 js 代码。
答:不会,因为有个预解析的过程,为了加快代码处理速度,将一些不会立即执行的代码延迟解析,是通过 parser 处理,不是 ignition 处理,所以猜想错误
问题:那么执行上下文是在要执行某段代码时,通过预扫描过程生成的吗?扫描的是 ast 还是字节码?
猜想:是的。所以有3个过程,ast 解析 -> ast 扫描 -> 字节码|机器码 文件执行
答:猜想错误。在 js 准备执行某段代码时,会执行如下过程
具体 ast 的应用,可以结合 babel, eslint 来看
弱类型语音支持隐式转换,强类型语言不支持隐式转换。
静态类型语言也有是弱类型的,比如 c、c++。
代码空间:存储代码语句
栈空间:存储调用栈,也就是存储的执行上下文对象,保存了变量数据。其中引用类型在这里保存的是一个引用地址值
堆空间:保存引用类型数据的真实值
渲染引擎在解析过程中,遇到 js script 标签,会将之前生成任务调用栈中的任务执行完全,然后才去解析执行下一个 script 标签内的 js 代码,如下面代码所示。这个同外链加载 js 一样,在加载之前会将之前加载的 js 代码执行了。
<script type="text/javascript">
setTimeout(function(){
console.log('setTimeout0')
}, 0)
console.log('shit')
console.log(add.name) // Uncaught ReferenceError: add is not defined
// 因为下面的js代码还没有解析,函数 add 还没有来得及函数声明提升,所以报错
</script>
<script type="text/javascript">
function add(){}
</script>
事件循环 见该文章详细说明