大话Chrome浏览器原理
一、一个页面为什么4个进程?
(1)主要原因
- 进程中的任何一个线程崩溃都会导致整个进程崩溃。
- 线程之间的数据时共享的,多页面使用多线程有安全性问题。
- 当一个进程关闭后资源的回收时候操作系统控制的,不易出现内存泄漏。
- 插件的崩溃会导致Chrome的不稳定。
- 所有模块都在一个进程导致Chrome不流畅。
(2)目前Chrome的进程架构
- 浏览器进程:主要负责用户界面显示、交互、子进程管理、存储。
- 渲染进程:使用Blink排版引擎和V8引擎渲染出页面,Chrome会为每一个Tab创建一个渲染进程,每个进程运行在沙箱中。
- GPU进程:初衷是实现CSS 3D、网页绘制和Chrome的UI部分。
- 网络进程:加载网络资源。
- 插件进程:负责插件的运行。
(3)当前的Chrome架构带来的问题
- 消耗资源
- 体系复杂
(4)未来面向服务的架构(SOA)
构建一个更加内聚、松耦合、易于维护和扩展的系统
Chrome正在构建操作系统化的Chrome基础服务,在性能强大的设备上使用多进程的方式运行基础服务,当在硬件资源受限的设备上使用Chrome的时候,就使用单进程的方式。
二、TCP协议如何保证页面传送到浏览器?
从TCP和UDP协议的传输过程思考,引出QUIC和HTTP3。
三、为什么第二次打开站点会很快?
浏览器发起HTTP请求的流程:
- 构建请求,首先是构建请求行。
- 查找缓存,查找浏览器缓存失败才会进行网络请求。
- 通过DNS准备IP地址和端口。
- 等待TCP队列,Chrome机制是同一个域名下最多只能建立6个连接,否则就会进入等待TCP队列。
- 建立TCP连接。
- 发送HTTP请求。
- 首先发送请求行,分别是请求方法、请求URI、HTTP协议版本。
- 如果是POST请求,那么还要发送请求体。
- 然后服务器返回数据,包括响应头和响应体。
- 通常情况下会断开TCP连接,如果在HTTP头部加入
Connection:Keep-Alive
,这样TCP就不会断开,连接可以被复用。
四、从输入URL到页面展示,中间发生了什么?
- 用户向浏览器输入URL,然后浏览器处理用户输入,判断是合法的URL地址还是搜索关键字,如果是关键字那么使用默认的搜索引擎构建搜索URL。
- 检查是否有缓存内容,否则浏览器通过进程间的IPC通信向网络进程发送请求信息。
- 网络进程接收服务器返回的数据,根据状态码判断是否进行重定向等操作。如果是301或302,那么表示需要重定向,此时网络进程会在请求头的Location字段中读取重定向的地址,再次发送网络请求。
- 通过响应头中的Content-Type判断进行何种操作。如果是下载类型,那么网络进程会把任务交给下载任务管理器。
- 由于服务器响应数据的时候就已经准备好了渲染进程,那么会将数据提交给渲染进程。Chrome会为每个站点打开一个渲染进程。
五、JavaScript、HTML和CSS如何变成页面?
由于渲染机制的复杂性,所以划分为许多子阶段,每个子阶段都有输入、输出和处理过程,这许多子阶段构成了渲染流水线。
(1)构建DOM
将HTML标签转化为DOM树,每个节点对应一个HTML标签。可以通过如下代码从Chrome开发者工具中获取当前页面的DOM树:
document
HTML解析器会随着文档的加载,边加载边解析。HTML解析器维护了一个Token栈结构用于计算父子节点之间的关系。
当解析到JavaScript的时候,DOM解析将会停止,执行代码,因为JavaScript有可能修改DOM结构。
Chrome在解析之前会有预解析线程先下载文档内嵌入的JavaScript下载链接。
在解析JavaScript之前首先要解析CSS文件,CSS文件加载会阻塞JavaScript脚本执行。
(2)样式计算
- 解析CSS文件。当渲染引擎接收到一个CSS文件的时候,会将CSS文本转换为样式表结构。可以通过如下代码在Chrome开发者工具中获取当前页面的样式表:
document.styleSheets
- 转换CSS属性值,使其标准化。比如2em会被转换为35px,HTML颜色会被转化RGB颜色。
- 计算出DOM树每个节点的样式。CSS具有继承和层叠规则。这些会在Chrome的Computed标签中显示。
(3)布局节点
- 创建布局树。由于HTML中还包含了许多不可见的元素,因此还需要创建一棵只包含可见元素的布局树。
- 然后将可见布局树和Computed CSS合成带有CSS的DOM树。
(4)图层树
渲染引擎还需要为特定的节点生成专门的图层,并生成一棵图层树。可以在Chrome的Layers标签中查看。
并不是每一个节点都会对应一个图层,如果一个节点没有图层,那么这个节点就属于父节点的图层。
- 拥有层叠上下文属性的HTML元素会提升为一个图层。
- 需要进行裁剪的HTML元素,比如overflow属性。
(5)图层绘制
渲染引擎会将图层树中的每个图层进行绘制,首先会将每一层的绘制拆分成许多绘制指令,然后绘制指令按照顺序组成待绘制列表。
(6)栅格化操作
-
主线程将待绘制列表准备好后提交给合成线程。通常情况下,一个页面可能很大,但是视口ViewPort是有限大的。因此合成线程会将图层划分为图块,通常是
256*256
或者512*512
。 - 合成线程会将视口附近的图块来优先生成位图,实际生成位图的操作由栅格化线程来执行。
- 栅格化线程通常情况下会使用GPU来完成,也叫快速栅格化。
- GPU生成的位图保存在GPU的显存中。浏览器中有个viz组件用来接收合成线程的DrawQuad命令,然后浏览器根据该命令将页面内容显示在屏幕上。
(7)3个重要概念
- 重排:更新元素的几何属性。通过CSS或者JS修改了元素的位置属性,那么就会触发浏览器重新布局,导致需要完整的渲染流水线。
- 重绘:更新元素的绘制属性。如果更改了页面的颜色属性,那么就会省去布局和分层阶段。
- 合成:比如使用了CSS的transform属性,那么就会避开重绘和重排。
六、JavaScript是按照顺序执行的吗?
变量提升:JavaScript解析引擎执行代码过程中,将变量的声明部分和函数的声明部分提升到代码开头的行为,变量提升以后会给变量设置默认值,这个默认是就是undefined
。
变量提升发生在编译阶段,在这个阶段会生成执行上下文和可执行代码。在执行上下文中保存了变量环境对象,该对象保存了变量提升的内容。
如果函数或者变量出现了重名,那么变量环境对象将会发生覆盖。
console.log(x);
var x = 10;
var x = 20;
f1();
function f1() {console.log('method: f1');}
function f1() {console.log('method: f1 override');}
f2();
var f2 = function() {console.log('method: f2');}
var f2 = function() {console.log('method: f2 override');}
undefined
method: f1 override
/Users/koils/test.js:9
f2();
^
TypeError: f2 is not a functio
七、为什么JavaScript会出现栈溢出?
在JavaScript中每个函数都有自己的执行上下文,JavaScript使用调用栈来管理这些执行上下文环境。全局执行上下文位于栈底。
调用栈是JavaScript引擎追踪函数执行的一个机制。
栈溢出:栈是有大小的,当入栈数目超过这个大小就会造成栈溢出现象。
八、作用域、作用域链和闭包
ES6中通过引入块级作用域配合let和const来避免变量提升这个设计缺陷。
作用域:是指在程序中定义变量的区域,这个位置决定了变量的生命周期。作用域是变量和函数的可访问范围。在ES6之前只有全局作用域和函数作用域,之后支持块级作用域。
变量提升带来的问题:
- 变量容易在不被察觉的情况下被覆盖掉。
- 本来应该销毁的变量没有被销毁。
JavaScript如何支持块级作用域?通过let声明的变量在编译阶段会被存放到词法环境,因此是使用词法环境和栈来支持的。
在JavaScript的每个执行上下文中都包含一个叫做outer的外部引用,用来指向外部的执行上下文。当进行变量查找的时候在当前作用域找不到就回去outer中查找,直到找到。这个查找链条叫做作用域链。
词法作用域:作用域由代码中的函数声明的位置来决定,是静态的作用域,通过这个作用域可以预测代码的执行过程。词法作用域在代码阶段就决定好了,与函数如何调用无关。
闭包:在JavaScript中,根据词法作用域的规则,内部函数总是可以访问其外部函数声明的变量,当通过调用一个外部函数返回一个内部函数的时候,即使该外部函数已经执行结束,但是内部函数引用外部函数变量依然保存在内存中,就把这些变量的集合称为闭包。
闭包的回收:如果闭包会一直被使用,那么可以当做全局变量存在。但是如果使用频率不高,而且占用内存较大,尽量让该闭包作为局部变量。
九、this
(1)全局执行上下文的this
全局执行上下文的this指向window对象。
(2)函数执行上下文的this
默认情况下也是指向window对象。设置this指向的方法有三种:
-
call方法,bind方法和apply方法。
-
通过对象调用位置:使用对象调用其内部的一个方法,该方法的this是指向对象本身的。
var object = { fn: function () {console.log(this);} }; object.fn();
-
通过构造函数:函数中的this属于新对象
function fn () {this.x = 'HelloWorld';} var object = new fn();
(3)this的设计缺陷
-
嵌套函数中this不会从外层函数中继承,解决方法有
- 将this体系转化为作用域体系
function fn () { this.x = 1; var that = this; function fx () { that.x = 10; } }
- 使用ES6中的箭头函数
function fn () { this.x = 1; var fx = () => {this.x = 10;}; }
- 普通函数的this指向window对象,这个问题可以通过使用严格模式解决。
十、JavaScript的内存机制
(1)数据存储
- 原始类型
类型 | 描述 |
---|---|
Boolean | 只有true和false两个值 |
Null | 只有一个值null,使用typeof检测时会返回object类型,这是JavaScript的Bug |
undefined | 一个没有被赋值的默认值,变量提升时也会使用该值 |
Number | 数字类型,64位二进制格式 |
BigInt | 可以用于表示任何精度 |
String | 表示文本数据,不可变 |
Symbol | 唯一且不可修改,通常用于作为Object和Key |
Object | 一组属性的集合 |
- 引用类型
JavaScript的内存空间分为栈空间、堆空间和代码空间。栈空间用于存储执行上下文。在JavaScript的赋值过程中,引用类型只会复制内存地址。
(2)垃圾回收
-
调用栈中的垃圾回收
JavaScript引擎通过向下移动ESP来销毁该函数保存在栈中执行的上下文。
-
堆中的垃圾回收
-
JavaScript使用垃圾回收器收集垃圾。待际假说:大部分对象在内存中存活时间会很短,不死的对象会活的更久。在V8引擎中分为新生代和老年代,新生代通常是1-8MB的内存空间,并且两个区域使用不同的GC机制。
- 新生代使用Scavenge算法,它将新生代划分为两个区域,一半是对象区域,另一半是空闲区域。当对象区域写满以后就进行GC,首先对对象区域的对象进行标记,然后再清理垃圾,副垃圾收集器将这些没有变成垃圾的对象复制到空闲区域,然后有序的排列,最后将对象区域和空闲区域进行角色翻转。
- JavaScript的主垃圾回收器主要进行老年代的垃圾回收工作,使用标记-清除算法。
- 当JavaScript的进行GC的时候,会产生StopTheWorld(全停顿)现象。由于老年代受到GC全停顿的影响较大,因此老年代的垃圾回收使用增量-标记算法,使得JavaScript脚本的执行和GC两个线程交替执行。
-
(3)解释编译
在JavaScript的执行引擎V8中,既有解释器(Ignition)也存在编译器(TurboFan)。
- 首先会从JavaScript代码翻译为AST并生成执行上下文。AST是⾮常重要的⼀种数据结构,在很多项⽬中有着⼴泛的应⽤。其中最著名的⼀个项⽬是Babel。的⼯作原理就是先将ES6源码转换为AST,然后再将ES6语法的AST 转换为ES5语法的AST,最后利⽤ES5的AST⽣成JavaScript源代码。
- 词法分析,生成Token。
- 语法分析,解析Token生成AST。
- 生成字节码。解释器根据AST解释并执行字节码。字节码是介于AST和机器码之间的一种代码,与特定类型的机器无关。
- 执行代码。多次重复执行的代码会选定为热点代码,由编译器编译为机器代码并保存。解释器Ignition在解释执⾏字节码 的同时,收集代码信息,当它发现某⼀部分代码变热了之后,TurboFan编译器把热点的字节 码转换为机器码,并把转换后的机器码保存起来,以备下次使⽤,这叫做JIT即时编译。
十一、消息队列和事件循环
- Chrome将事件存放到队列,然后使用循环机制将消息取出,然后执行。比如渲染进程专门有一个IO线程用于通过队列接受其他线程传来的任务。
- 当线程需要安全的退出的时候,由于在进程中设置了退出标志,每次在队列中取出任务执行之前都需要检查标志。
- 对于高优先级任务的处理,比如监听DOM节点的变化情况,会作为微任务添加到队列中宏任务的微任务队列中,当任务执行完成后检查当前任务的微任务队列是否存在微任务,有就取出来执行。
- 通过Promise和MutationObserver监控某个DOM节点都会产生微任务。
十二、JavaScript面向对象
(1)封装
由于JavaScript没有提供权限访问修饰符,因此可以通过闭包的方式实现私有变量的保护:
function Book(name) {
this.getName = () => {return name;}
this.setName = (x) => {name = x;}
}
let book = new Book("HelloWorld");
book.setName("JavaScript");
book.getName();
(2)继承
在ES6之前,没有extends关键字,最常见的叫做原型链继承。原型prototype是JavaScript函数中的一个内置属性,指向另外一个对象,被指向的对象的所有的属性和方法都会被当前的实例所继承。
- 设置prototype的代码需要放到构造器之外。
- 设置prototype的代码需要放到任何实例化之前。
原型链继承无法解决父类构造方法存在参数的问题,因此可以通过构造继承解决:
function Base1(name) {this.name = name;}
function Base2(age) {this.age = age;}
function Child(name, age) {
Base1.call(this, name);
Base2.call(this, age);
}
(3)多态
- 当创建类的实例的时候,没有使用new关键字,this指的是window对象,否则指向的是当前实例对象。
- 当类存在return语句的时候,如果返回的是基本数据类型,那么this就会强制指定为当前类对象;如果返回的是引用数据类型,那么会遵循return语句。
十三、setTimeout实现原理
Chrome中使用延迟队列保存Chrome内部的延时任务和setTimeout提交的延时任务。当执行完消息队列中的任务之后就会开始执行延时队列的处理函数,然后延时队列处理函数会根据发起时间和延迟时间计算出到期任务。
使用setTimeout的注意事项:
-
如果当前任务执行时间过久,会影响到定时器的执行。
-
如果setTimout存在嵌套,那么系统会设置4ms的间隔时间。
-
当前页面标签如果没有被**,那么setTimeout的执行最小时间间隔是1s。目的是优化加载消耗和耗电量。
-
延迟执行时间有最大值,当延时24.8天setTimeout就会溢出,因为setTimeout使用的是32bit来存储。
-
setTimeout执行的函数this对象指向window,可以通过匿名函数或者bind方法解决:
setTimeout(function() {}, 1); setTimeout(() => {}, 1); setTimeout(object.func.bind(object), 1);
十四、浏览器缓存
(1)强缓存与协商缓存
在浏览器中分为强缓存和协商缓存。强缓存不需要发送HTTP请求,当检查是否是强缓存的时候在HTTP1.0和HTTP1.1中是不一样的:
- 早期的HTTP1.0使用的是Expires字段,它指明了过期时间。
- HTTP1.1使用的是Cache-Control字段,有以下参数:
- 通过max-age指明缓存存活时间。
- private表示只有浏览器才能缓存,中间代理服务器无法缓存。
- no-cache表示跳过强缓存,直接进入协商缓存阶段
- no-store表示直接不进行缓存
- s-maxage表示针对代理服务器的缓存时长
- Expires和Cache-Control同时存在的时候,优先考虑Cache-Control
当强缓存失效之后,浏览器在请求头中携带相应的缓存tag
来向服务器发请求,由服务器根据这个tag,来决定是否使用缓存,这就是协商缓存。
- Last-Modified:即最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。浏览器接收到后,如果再次请求,会在请求头中携带
If-Modified-Since
字段,这个字段的值也就是服务器传来的最后修改时间。服务器拿到请求头中的If-Modified-Since
的字段后,其实会和这个服务器中Last-Modified
对比:- 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
- 否则返回304,告诉浏览器直接用缓存。
- ETag:
ETag
是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头
把这个值给浏览器。浏览器接收到ETag
的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器。服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对:- 如果两者不一样,说明要更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。
- 否则返回304,告诉浏览器直接用缓存。
在精准度上,ETag优于Last-Modified。优于 ETag 是按照内容给资源上标识,因此能准确感知资源的变化。Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。
在性能上,Last-Modified优于ETag,也很简单理解,Last-Modified仅仅只是记录一个时间点,而 Etag需要根据文件的具体内容生成哈希值。
(2)Service Worker Cache
Service Worker 借鉴了 Web Worker的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM。虽然如此,但它仍然能帮助我们完成很多有用的功能,比如离线缓存、消息推送和网络代理等功能。其中的离线缓存就是 Service Worker Cache。Service Worker 同时也是 PWA 的重要实现机制。
(3)Memory Cache
内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,当渲染进程结束后,内存缓存也就不存在了。
(4)Disk Cache
存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长。
(5)Push Cache
即推送缓存,这是浏览器缓存的最后一道防线。它是 HTTP/2中的内容,虽然现在应用的并不广泛,但随着 HTTP/2 的推广,它的应用越来越广泛。
十五、浏览器存储
(1)Cookie
Cookie 本质上就是浏览器里面存储的一个很小的文本文件,内部以键值对的方式来存储。向同一个域名下发送请求,都会携带相同的 Cookie,服务器拿到 Cookie 进行解析,便能拿到客户端的状态。
Cookie就是用来做状态存储的。缺陷如下:
- 容量缺陷。Cookie 的体积上限只有4KB。
- 性能缺陷。Cookie 紧跟域名,不管域名下面的某一个地址需不需要这个 Cookie ,请求都会携带上完整的 Cookie,这样随着请求数的增多,其实会造成巨大的性能浪费的,因为请求携带了很多不必要的内容。
- 安全缺陷。由于 Cookie 以纯文本的形式在浏览器和服务器中传递。在HttpOnly为 false 的情况下,Cookie 信息能直接通过 JS 脚本来读取。
(2)localStorage
也是针对一个域名,即在同一个域名下,会存储相同的一段localStorage。与Cookie的区别如下:
- 容量。localStorage 的容量上限为5M。对于一个域名是持久存储的。
- 只存在客户端,默认不参与与服务端的通信。这样就很好地避免了 Cookie 带来的性能问题和安全问题。
- 接口封装。通过localStorage暴露在全局,并通过它的 setItem 和 getItem等方法进行操作。
(3)sessionStorage
- 容量。容量上限也为 5M。
- 只存在客户端,默认不参与与服务端的通信。
- 接口封装。
但sessionStorage
和localStorage
有一个本质的区别,那就是前者只是会话级别的存储,并不是持久化存储。会话结束,也就是页面关闭,这部分sessionStorage
就不复存在了。
(4)IndexedDB
IndexedDB是运行在浏览器中的非关系型数据库, 本质上是数据库,理论上这个容量是没有上限的。支持事务和二进制存储。
- 键值对存储,内部采用对象仓库存储方式。
- 异步操作,数据库的读写属于IO操作,浏览器提供了异步IO支持。
- 受到同源策略限制,无法跨域访问数据库。
上一篇: 有魔法的青蛙
下一篇: WIN10编译Chromiumbb记录
推荐阅读
-
探究为何rem在chrome浏览器上计算出错_html/css_WEB-ITnose
-
如何用chrome在电脑上模拟微信内置浏览器
-
Ubuntu、Linux Mint一键安装Chrome浏览器的Shell脚本分享
-
js汉字排序问题 支持中英文混排,兼容各浏览器,包括CHROME_javascript技巧
-
canvas在浏览器里的渲染原理?
-
浏览器的渲染原理简介_html/css_WEB-ITnose
-
selenium使用chrome浏览器测试(附chromedriver与chrome的对应关系表)
-
解决ExtJS在chrome或火狐中正常显示在ie中不显示的浏览器兼容问题_extjs
-
CSS滚动条样式如何兼容IE8和Chrome浏览器?
-
支持IE,firefxo,chrome浏览器下鼠标拖动和拖拽的鼠标指针特效