这一部分展开的内容是:浏览器进程/线程模型,JS 运行机制。
浏览器是多进程的,有一个主控进程,以及每一个 Tab 页都会新开一个进程(某些情况下多个 Tab 会合并进程);进程可能包括主控进程、插件进程、GPU、Tab 页(浏览器内核)等:
每一个 Tab 页可以看作是浏览器内核进程,然后这个进程是多线程的,它有几大类子线程:
输入 URL 后,会进行解析(URL 的本质就是统一资源定位符),URL 一般包括几大部分:
每次网络请求时都需要开辟单独的线程进行,如果 URL 解析到 HTTP 协议,就会新建一个网络线程去处理资源下载。
更多参考: 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理 。
这一部分主要内容包括:DNS 查询,TCP/IP 请求构建,五层因特网协议栈等。
如果输入的是域名,需要进行 DNS 解析成 IP,大致流程:
注意:域名查询时有可能是经过了 CDN 调度器的(如果有 CDN 存储功能的话);DNS 解析是很耗时的,如果解析域名过多,会让首屏加载变得过慢,可以考虑 dns-prefetch 优化。
HTTP 的本质就是 TCP/IP 请求,TCP 将 HTTP 长报文划分为短报文,通过三次握手与服务端建立连接,进行可靠传输。
建立连接成功后,接下来就正式传输数据;待到断开连接时,需要进行四次挥手。
浏览器对同一域名下并发的 TCP 连接是有限制的(2-10 个不等);在 HTTP 1.0 中往往一个资源下载就需要对应一个 TCP/IP 请求,所以针对这个瓶颈,又出现了很多的资源优化方案。
GET 和 POST 虽然本质都是 TCP/IP,但两者除了在 HTTP 层面外,在 TCP/IP 层面也有区别;GET 会产生一个 TCP 数据包,POST 两个,具体就是:
从应用层的发送 HTTP 请求,到传输层通过三次握手建立 TCP/IP 连接,再到网络层的 IP 寻址,再到数据链路层的封装成帧,最后到物理层的利用物理介质传输。
五层因特网协议栈其实就是:
当然,其实也有一个完整的 OSI 七层框架,与之相比,多了会话层、表示层。
OSI 七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层:
服务端在接收到请求时,内部会进行很多的处理。
对于大型的项目,由于并发访问量很大,所以往往一台服务器是吃不消的,所以一般会有若干台服务器组成一个集群,然后配合反向代理实现负载均衡,当然了,负载均衡不止这一种实现方式,这里不深入。
用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了 nginx 控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的 HTTP 响应,并将它反馈给用户。
一般后台都是部署到容器中的,所以一般为:
概括下:
前后端交互时,HTTP 报文作为信息的载体,所以 HTTP 是一块很重要的内容。
报文一般包括了:通用头部、请求/响应头部、请求/响应体。
譬如,在跨域拒绝时,可能是 Method 为 OPTIONS,状态码为 404/405 等。
不同范围状态的意义:
常用的请求头部:
常用的响应头部:
一般来说,请求头部和响应头部是匹配分析的;譬如,请求头部的 Accept 要和响应头部的 Content-Type 匹配,否则会报错。
HTTP 请求时,除了头部,还有消息实体,一般来说,请求实体中会将一些需要的参数都放入;譬如实体中可以放参数的序列化形式(a=1&b=2),或者直接放表单对象(FormData 对象,上传时可以夹杂参数以及文件);而一般响应实体中,就是放服务端需要传给客户端的内容。
Cookie 是浏览器的一种本地存储方式,一般用来帮助客户端和服务端通信的,常用来进行身份校验,结合服务端的 session 使用:
上述就是 Cookie 的常用场景简述(当然了,实际情况下得考虑更多因素)。
一般来说,Cookie 是不允许存放敏感信息的(千万不要明文存储用户名、密码),因为非常不安全;如果一定要强行存储,首先,一定要在 Cookie 中设置 HTTPOnly(这样就无法通过 JS 操作了),另外可以考虑 RSA 等非对称加密(因为浏览器本地也是容易被攻克的,并不安全)。
另外,由于在同域名的资源请求时,浏览器会默认带上本地的 Cookie,针对这种情况,在某些场景下是需要优化的:
此时就造成了较为严重的浪费,而且也降低了访问速度;当然了,针对这种场景,是有优化方案的(多域名拆分)。具体做法就是:
说到了多域名拆分,这里再提一个问题,那就是:
首先,明确 gzip 是一种压缩格式,需要浏览器支持才有效,而且 gzip 压缩效率很好(高达 70% 左右);gzip 一般是由 apache、tomcat 等服务器开启。
除了 gzip 外,也还会有其它压缩格式(如 deflate,没有 gzip 高效,且不流行),所以一般只需要在服务器上开启了 gzip 压缩,之后的请求就都是基于 gzip 压缩格式的,非常方便。
gzip 有(0~9)10 个压缩机别,压缩级别越高,压缩效果越好,也越占 CPU;考虑都性能和压缩率的取舍,压缩级别不易设的太高。
TCP/IP 层面的定义:
HTTP 层面的定义:
注意:keep-alive 不会永远保持,它有一个持续时间,一般在服务器中配置(如 apache);另外长连接需要客户端和服务器都支持时才有效。
HTTP/2 与 HTTP/1.x 的显著不同点:
所以,如果 HTTP/2 全面应用,很多 HTTP/1.x 中的优化方案就无需用到了(如打包成精灵图、静态资源多域名拆分等)。
简述下 HTTP/2 的一些特性:
HTTPS 就是安全版本的 HTTP,譬如一些支付等操作基本都是基于 HTTPS 的,因为 HTTP 请求的安全系数太低了;简单来看,HTTPS 与 HTTP 的区别就是:在请求前,会建立 SSL 链接,确保接下来的通信都是加密的,无法被轻易截取分析。
SSL/TLS 的握手流程:
浏览器请求建立 SSL 链接,并向服务端发送一个随机数 ClientRandom 和客户端支持的加密方法,比如 RSA 加密,此时是明文传输;
服务端从中选出一组加密算法与 Hash 算法,回复一个随机数 ServerRandom,并将自己的身份信息以证书的形式发回给浏览器(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息);
浏览器收到服务端的证书后:
服务端收到浏览器的回复:
浏览器解密并计算握手消息的 Hash,如果与服务端发来的 Hash 一致,此时握手过程结束。
之后所有的 HTTPS 通信数据将由之前浏览器生成的 key 并利用对称加密算法进行加密。
前后端的 HTTP 交互中,使用缓存能很大程度上的提升效率:
对于协商缓存,强制刷新可以使得缓存无效;对于强缓存,在未过期时,必须更新资源路径才能发起新的请求。各大缓存头部的整体关系如下图:
浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:
这个过程可以简述如下:bytes → characters → tokens → nodes → DOM;列举一些重点过程:
CSS 规则树的生成也是类似;简述为:bytes → characters → tokens → nodes → CSSOM。
当 DOM 树和 CSSOM 都有了后,就要开始构建渲染树了;一般来说,渲染树和 DOM 树相对应的,但不是严格意义上的一一对应;有一些不可见的 DOM 元素不会插入到渲染树中,如 head 这种不可见的标签或者 display:none 等。
注意:DOM 解析和 CSS 解析是两个并行的进程,所以 CSS 加载不会阻塞 DOM 树的解析;RenderTree 是依赖于 DOMTree 和 CSSOMTree 的,所以无论 DOMTree 是否已经完成,它都必须等待到 CSSOMTree 构建完成,即 CSS 加载完成(或 CSS 加载失败)后,才能开始渲染。
有了渲染树,接下来就是开始渲染,基本流程如下:
图中的线与箭头代表通过 JS 动态修改了 DOM 或 CSS,导致了 Reflow 或 Repaint:
回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流,所以优化方案中一般都包括“尽量避免回流”。
什么会引起回流:
回流优化方案:
示例:
|
|
上述中的渲染中止步于绘制,但实际上绘制这一步也没有这么简单,它可以结合复合层和简单层的概念来讲:
更多参考: 普通图层和复合图层
实际上,在解析 HTML 时,会遇到一些资源连接,此时就需要进行单独处理了;简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):
当遇到上述的外链时,会单独开启一个下载线程去下载资源(HTTP/1.x 中,每一个资源的下载都要开启一个请求,对应一个 TCP/IP 链接)。
异步下载,不会阻塞解析,下载完毕后直接用图片替换原有 src 的地方。
CSS 可视化格式模型就是规定了浏览器在页面中如何处理文档树:
关键字:包含块(Containing Block)、控制框(Controlling Box)、BFC(Block Formatting Context)、IFC(Inline Formatting Context)、定位体系、浮动…
一个元素的 Box 的定位和尺寸,会与某一矩形框有关,这个框就称之为包含块。元素会为它的子孙元素创建包含块,但是并不是说元素的包含块就是它的父元素,元素的包含块与它的祖先元素的样式等有关系:
块级元素和块框以及行内元素和行框的相关概念。
FC 即格式上下文,它定义框内部的元素渲染规则,比较抽象,譬如:
在块格式化上下文中,每一个元素左外边与包含块的左边相接触(对于从右到左的格式化,右外边接触右边);即使存在浮动也是如此(所以浮动元素正常会直接贴近它的包含块的左边,与普通元素重合),除非这个元素也创建了一个新的 BFC。
BFC 特点:
如何触发 BFC?
注意:display:table 本身不产生 BFC,但是它会产生匿名框(包含 display:table-cell 的框),而这个匿名框产生 BFC。
前面有提到遇到 JS 脚本时,会等到它的执行,实际上是需要引擎解析的,这里展开描述。
JS 是解释型语音,所以它无需提前编译,而是由解释器实时运行,引擎对 JS 的处理过程可以简述如下:
现代浏览器一般采用即时编译(JIT-Just In Time Compiler),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存。
在正式执行 JS 前,还会有一个预处理阶段(譬如变量提升、分号补全等),确保 JS 可以正确执行,这里仅提部分:
JS 执行是需要分号的,但为什么以下语句却可以正常运行呢?
|
|
原因就是 JS 解释器有一个 Semicolon Insertion 规则,它会按照一定规则,在适当的位置补充分号。
一般包括函数提升和变量提升,譬如:
|
|
经过变量提升后,就变成:
|
|
此段内容中的图片来源: 深入理解 JavaScript 系列(10):JavaScript 核心
解释器解释完语法规则后,就开始执行,整个执行流程中大致包含:
如果程序执行完毕,被弹出执行栈,然后又没有被引用(没有形成闭包),那么这个函数中用到的内存就会被垃圾处理器自动回收。
每一个执行上下文,都有三个重要属性:
只有全局上下文的变量对象允许通过 VO 的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象);AO(Activation Object),当函数被调用者激活,AO 就被创建了。
它是执行上下文中的一个属性,原理和原型链很相似,作用很重要,譬如流程简述:在函数上下文中,查找一个变量 foo;如果在函数的 VO 中找到了,就直接使用;否则去它的父级作用域链中找;如果父级中没找到,继续往上找;直到全局上下文中也没找到就报错。
this 是执行上下文环境的一个属性,而不是某个变量对象的属性:
JS 有垃圾处理器,所以无需手动回收内存;而是由垃圾处理器自动处理,常用的两种垃圾回收规则是:
JS 引擎基础 GC 方案是 Mark and Sweep(标记清除),简单解释如下:
和其他语言一样,JS 的 GC 策略也无法避免一个问题:垃圾回收时,停止响应其他操作;这是为了安全考虑。而 JS 的 GC 在 100ms 甚至以上,对一般的应用还好,但对于 JS 游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是引擎需要优化的点:避免 GC 造成的长时间停止响应。
目的是通过区分“临时”与“持久”对象:
更多参考: V8 内存浅析
譬如发出网络请求时,会用 ajax,如果接口跨域,就会遇到跨域问题,可以参考:
譬如浏览器在解析 HTML 时,有 XSSAuditor,可以延伸到安全相关领域,可以参考: