这里的浏览器特指Chrome浏览器,JS引擎特指V8。
一、浏览器多进程架构
二、TCP/IP 协议
主要讲 TCP 连接的三个过程:建立连接、传输数据、断开连接,IP是传输数据包用的。
可以关注一下七层协议模型。
三、HTTP 请求过程
(一)浏览器发起 HTTP 请求的过程
- 构建请求
- 查找缓存
- 准备IP和端口(DNS解析)
- 等待 TCP 队列
- 建立 TCP 连接
- 发起 HTTP 请求
(二)服务端响应请求的过程
- 服务端返回请求
- 断开连接
HTTP缓存和登录状态
特殊情况:重定向。
四、从URL到页面展示发生了什么?
(一)用户输入
- 浏览器进程分析用户输入是关键字还是URL
如果是关键字,将其组装进默认搜索引擎的URL中进行搜索 - 如果是URL,则构建 URL 请求并通过 IPC 通信将请求交给网络进程
(二)URL请求过程
- 查缓存
- 准备IP和端口(DNS解析)
- TCP连接和TLS连接
该过程可能会等待 TCP 队列 - 构建请求行和请求头并发起请求
- 服务端处理请求并响应
- 网络进程读取响应头信息
- 根据状态码做不同反应,400、500就通知浏览器进程报错,301、302就进入重定向,304缓存,200则正常。
- 如果正常或者缓存,则进一步查看 Content-type,针对不同的content-type,做不同的处理;如果是 HTML 就通知浏览器进程准备渲染进程。
(三)准备渲染进程
一般一个标签页一个渲染进程,如果内部有 iframe,则会另外再划分出单独的渲染进程(站点隔离)。准备好以后,浏览器进程就通知渲染进程提交文档。
(四)提交文档
首先要明确一点,这里的“文档”是指 URL 请求的响应体数据。
- 渲染进程接收到“提交文档”的消息后,会和网络进程建立传输数据的“管道”。
- 等文档数据传输完成之后,渲染进程会返回“确认提交”的消息给浏览器进程。
- 浏览器进程在收到“确认提交”的消息后,会更新浏览器界面状态,包括了安全状态(黑白名单、跨站攻击等)、地址栏的 URL、前进后退的历史状态,并更新 Web 页面。
到此,导航阶段结束。
(五)渲染阶段
(1)HTML Parse
构建 DOM 树,document 对象可以看到 DOM 结构
(2)Recalculate Style
document.styleSheets
- 把 CSS 转换为浏览器能够理解的结构
- 转换样式表中的属性值,使其标准化
- 计算 DOM 树中每个节点的具体样式(ComputedStyle)
这里涉及到了 CSS 样式的继承和层叠
(3)Layout
- 创建布局树(Layout Tree)
通过 DOM 和 ComputedStyle,遍历所有可见元素,去除所有不可见元素,构建布局树;不可见元素举例:
head标签
、display: none
- 布局计算
计算布局树中每个节点的坐标位置
(4)Update Layer Tree
通常情况下,并不是布局树的每个节点都包含一个图层,如果一个节点没有对应的层,那么这个节点就从属于父节点的图层:
- 拥有层叠上下文属性的元素会被提升为单独的一层
- 需要剪裁(clip)的地方也会被创建为图层
(5)Paint
渲染引擎实现图层的绘制,会把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表。
(6)Composite Layers
Paint 只是生成了用来记录绘制顺序和绘制指令的列表,而实际上绘制操作是由渲染引擎中的合成线程来完成的。Composite Layers主要涉及到以下两个操作:
-
合成线程先将图层划分为图块(tile)
这些图块的大小通常是 256x256 或者 512x512。
-
然后先将视口附近的图块栅格化
所谓栅格化,是指将图块转换为位图。而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的。通常,栅格化过程都会使用 GPU 来加速生成,使用 GPU 生成位图的过程叫快速栅格化,或者 GPU 栅格化,生成的位图被保存在 GPU 内存中。
GPU 操作是运行在 GPU 进程中,如果栅格化操作使用了 GPU,那么最终生成位图的操作是在 GPU 中完成的,这就涉及到了跨进程操作。具体形式可以参考下图:
(7)显示
一旦所有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,然后将该命令提交给浏览器进程。
浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到内存中,最后再将内存显示在屏幕上。
备注:一个完备的渲染过程大概会经历以上阶段;此外,实际的每一帧的渲染还有其他一些细节,比方说 rAF 和 rIC;页面异步阶段,还会监听事件,页面解析之前还会执行 JS。
重排、重绘与合成
- 重排(更新了元素的几何属性)
- 重绘(更新了元素的绘制属性)
- 直接合成阶段
五、JS的执行上下文
(一)JS 是编译执行的
一边编译一边执行,首次编译执行的是全局代码,遇到函数时再又编译执行。
(二)编译时生成执行上下文
执行上下文包含以下内容:
- 变量环境,包含了以下部分:
- 全局变量或者局部变量
- outer,用来指向外部执行上下文,从而构成作用域链 - 词法环境,用来管理块级变量
- this
1. 全局上下文中,指向全局对象
2. 普通函数调用,指向全局对象
3. 箭头函数中的 this
4. 构造函数中的 this
5. 通过 call、apply、bind 更改后的 this
6. 作为对象方法调用时,指向对象
7. eval 中的 this
(三)调用栈管理执行上下文
注意,调用栈中的变量环境只保存了原始类型的变量值,对于引用类型只保存引用,引用类型的值保存在堆空间中。
(四)闭包的概念
闭包就是对象,存储在堆空间中,用来收集内部函数引用的外部变量。
进一步参考:闭包的内存模型
闭包极容易产生内存泄露,要注意合理使用。
六、JS的内存管理
(一)JS 是动态弱类型语言
- 动态是指在运行过程中需要检查数据类型的语言,在使用之前就需要确认其变量数据类型的称为静态语言。
- 弱类型是指支持隐式类型转换,不支持隐式类型转换的语言称为强类型语言
- JS 具有两种数据类型
- 原始类型:null、undefined、Boolean、Number、String、Symbol、BigInt
- 引用类型
(二)JS 内存分配
(1)三种内存类型:代码空间、栈空间、堆空间。
-
栈空间
用来存储执行上下文,上下文中又存储了变量环境、词法环境和this,其中变量环境中会直接保存原始类型的值 -
堆空间
引用类型的数据在变量环境中只是存储引用,实际值存储在堆空间
之所以分为栈空间和堆空间,是因为栈空间用来维护程序执行期间的上下文状态,而为了快速切换执行上下文状态,栈空间不会设置太大。引用类型的数据通常比较大,所以放入堆空间;堆空间很大,可以存放很多数据,但缺点是分配内存和回收内存都会占用一定时间。
(2)“闭包”的内存模型
闭包的产生有两个过程:
-
第一步对内部函数进行词法扫描
如果有闭包,则在变量环境中创建闭包对象closure(foo)
(它是一个内部对象,JavaScript无法访问),由于它是引用类型,所以其值被保存在堆空间中 -
第二步把内部函数引用的外部变量保存到堆中的闭包对象
closure(foo)
上
(三)JS 内存回收
不再使用的数据就是垃圾数据,其占用的内存空间需要被回收,否则会造成内存泄漏。
与内存分配相对应,内存回收主要考虑调用栈数据与堆数据的回收。
垃圾回收的有以下两种策略:
- 手动回收,何时分配内存、何时回收内存都是通过代码控制的
比如说:C、C++- 自动回收,由垃圾回收器回收
比如说:Java、Python、JavaScript
(1)调用栈数据的回收
这个比较简单,通过向下移动 ESP 来销毁该函数在栈中保存的执行上下文。
ESP 是汇编语言关键字,表示栈顶指针;相对应的是 EBP,表示栈底指针
(2)堆数据的回收
需要通过垃圾回收器来回收。
垃圾回收的工作流程
- 标记空间中的活动对象和非活动对象
- 回收非活动对象所占据的内存
- 内存整理
一般来说,频繁的回收对象造成内存从存在大量不连续的空间,我们把这些空间称之为内存碎片。当内存中有大量的内存碎片时,如果需要分配较大连续内存,可能会出现内存不足的情况。所以需要进行内存整理,这一步是可选的,比如:副垃圾回收器不需要单独进行整理。
代际假说与新老生分区
它建立在代际假说之上:
- 第一是大部分对象在内存中存在时间很短
- 第二是不死的对象会存在很久
基于这个假说,V8 将堆空间分为新生区和老生区:
- 新生区存放的是生存时间短的对象,新生代通常支持
1-8M
的容量 - 老生区存放的是生存时间久的对象,老生代支持的容量会大很多
针对新生区和老生区分别使用不同的垃圾回收器,以便更高效地实施垃圾回收:
- 副垃圾回收器负责新生区的垃圾回收
- 主垃圾回收器负责老生区的垃圾回收
副垃圾回收器
负责新生区的垃圾回收。
通常情况下,大多数小的对象都是分配在这一区域,虽然不大,但是回收很频繁。
副垃圾回收器采用Scavenge算法,它将新生区一分为二,一半是对象区域,一半是空闲区域:
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
回收过程如下:
- 标记对象区域中的垃圾
- 标记完成后,直接复制存活对象到空闲区域中,然后翻转对象区域与空闲区域的角色
如此,既清除了垃圾数据,又整理了内存空间。
由于复制需要花费时间,所以新生区会被设置的比较小,这也使得它很容易被存活的对象填满。为了解决这个问题,JS 引擎将存活两次的对象晋升到老生区中,这种策略被称之为对象晋升策略。
主垃圾回收器
负责老生区的垃圾回收。
除了新生区中的晋升对象,还有一些大的对象会直接放到老生区中。
老生区具有以下两个特点:
- 对象占用空间大
- 对象存活时间长
由于这两个特点,再采用Scavenge算法已然不合适——复制时间长,而且还浪费一半的空间。
主垃圾回收器采用标记-清除法(Mark-Sweep),回收过程如下:
-
标记对象区域中的垃圾
ESP 向下移动,这时候如果开始标记,它会遍历整个调用栈,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
-
将标记的垃圾清除
为了解决标记-清除法清除过程中产生的内存碎片,于是又产生了另外一种算法——标记-整理法,两者标记过程相同,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
全停顿与增量标记法
由于 JavaScript 运行在主线程之上,所以一旦开启垃圾回收,就需要暂停整个 JS 脚本的执行,待垃圾回收完毕后,再恢复脚本执行。我们把这种行为称之为全停顿(Stop-The-World)
显然,全停顿过长会严重影响应用的性能和响应能力。
对于新生区来说,由于其空间小,存活的对象少,所以全停顿的影响不大。
但老生区与之相反,为此 V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JS 脚本交替执行,直到标记阶段完成,我们把这个算法称之为增量(Incremental Marking)算法。
扩展:除了增量算法以外,V8 还会采用多线程的方式进行并行标记。
七、JS的编译执行
编译器和解释器
(一)生成抽象语法树(AST)和执行上下文
第一阶段是分词(tokenize)
又称为词法分析,其作用是将一行行的源码拆分成一个个token。
所谓token,指的是语法上不可能再分的、最小的单个字符或字符串。
第二阶段是解析(parse)
又称为语法分析,其作用是将上一步生成的 token 根据语法规则转为 AST。
如果没有语法错误,就顺利生成 AST。否则,就终止解析并抛出一个“语法错误”。
生成 AST 之后,再生成执行上下文
(二)生成字节码
有了 AST 和执行上下文,解释器Ignition会根据AST生成字节码
字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
(三)执行字节码
执行过程采用“字节码 + JIT”技术,即解释器逐条解释执行字节码,在执行过程中,发现热点代码,编译器就会将其编译为高效的机器码。
八、页面循环系统
渲染进程中的主线程,会开启一个事件循环。当无任务执行时,进入休眠;有任务时,会被激活执行任务。
任务分为两种:宏任务和微任务,前者放入消息队列中,后者存储在微任务队列。此外,还有延迟任务队列,用来实现 setTimeout。
微任务队列与每一个宏任务关联。微任务的执行时机是当前主函数结束之后,宏任务结束之前。
任务类型参考Chromium code。
微任务:Promise(await)、process.nextTick、queueMicrotask()、MutationObserver。
循环系统参考下图:
九、浏览器中的页面
(一)Chrome 开发工具的介绍
略
(二)JavaScript 对页面渲染的影响
DOM 树构建的细节
- 网络进程与渲染进程之间的数据管道
- 渲染进程中的 HTML Parser 的处理细节
JavaScript 阻塞 DOM,同时又被 CSSOM 阻塞
(三)CSS 对页面渲染的影响
(四)页面的分层合成机制
(五)从渲染流水线角度看页面性能优化
(六)虚拟DOM
(七)PWA
Service worker 与 manifest
(八)Web Component
自定义元素、shadow dom、HTML模板
十、浏览器中的网络
HTTP0.9、HTTP1.0、HTTP1.1、HTTP2、HTTP3
(一)HTTP1.1 的缺陷
- 最大连接数的限制
- 队头阻塞的问题
- TCP 连接慢启动
- TCP 连接之间有网络资源竞争的问题
(二)HTTP2
多路复用
一个域名只使用一个 TCP 长连接和消除队头阻塞问题。
- 一个域名一个TCP连接,只需一次慢启动,同时也不会存在网络资源竞争问题。
- 资源并行请求,消除队头阻塞
通过二进制分帧层实现HTTP2
设置请求的优先级
服务器推送
头部压缩
(三)HTTP3
- TCP连接的队头阻塞问题
有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。 - TCP建立连接的延迟
- TCP协议僵化
HTTP3 本质的改变:
十一、浏览器安全
(一)Web 页面安全
同源策略
协议、域名、端口都相同则为同源。
同源策略表现在三个方面:
- DOM 层面 —— 跨域JavaScript脚本限制操作DOM
- 数据层面 —— 跨域数据禁止读取
- 网络层面 —— 跨域网络
同源策略限制和便利性的权衡:
- 页面中可以嵌入第三方资源——引入了
CSP(Content Security Policy)
增加了安全性 - 跨域资源共享——
CORS(Cross-origin resource sharing)
针对网络层面的同源策略 - 跨文档通信——
postMessage
针对DOM层面的同源策略
XSS攻击
XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。
XSS攻击可以做哪些事情?
- 可以窃取 Cookie 信息
- 可以监听用户行为
- 可以通过修改 DOM伪造假的登录窗口,用来欺骗用户输入用户名和密码等信息
- 还可以在页面内生成浮窗广告
恶意脚本是怎么注入的?
- 存储型XSS攻击
- 反射性 XSS 攻击
在一个反射型 XSS 攻击过程中,恶意 JavaScript 脚本属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript 脚本返回给用户。当恶意 JavaScript 脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作。 - 基于DOM的XSS攻击
基于 DOM 的 XSS 攻击是不牵涉到页面 Web 服务器的。具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。
如何组织XSS攻击?
- 服务器对输入脚本进行过滤或转码
- 充分利用CSP
- 限制加载其他域下的资源文件
- 禁止向第三方域提交数据
- 禁止执行内联脚本和未授权脚本
- 还提供了上报机制
- http-only 的 cookie
- 验证码
CSRF 攻击
(二)安全沙箱:页面和系统之间的隔离墙
浏览器中的安全沙箱是利用操作系统提供的安全技术,让渲染进程在执行过程中无法访问或者修改操作系统中的数据,在渲染进程需要访问系统资源的时候,需要通过浏览器内核来实现,然后将访问的结果通过 IPC 转发给渲染进程。
具体影响有三点:
- 持久存储(cookie、缓存)
- 网络访问
- 用户交互