欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

大话Chrome浏览器原理

程序员文章站 2022-07-01 12:02:14
...

一、一个页面为什么4个进程?

(1)主要原因

  • 进程中的任何一个线程崩溃都会导致整个进程崩溃
  • 线程之间的数据时共享的,多页面使用多线程有安全性问题。
  • 当一个进程关闭后资源的回收时候操作系统控制的,不易出现内存泄漏
  • 插件的崩溃会导致Chrome的不稳定。
  • 所有模块都在一个进程导致Chrome不流畅

(2)目前Chrome的进程架构

  • 浏览器进程:主要负责用户界面显示、交互、子进程管理、存储。
  • 渲染进程:使用Blink排版引擎和V8引擎渲染出页面,Chrome会为每一个Tab创建一个渲染进程,每个进程运行在沙箱中。
  • GPU进程:初衷是实现CSS 3D、网页绘制和Chrome的UI部分。
  • 网络进程:加载网络资源。
  • 插件进程:负责插件的运行。

(3)当前的Chrome架构带来的问题

  • 消耗资源
  • 体系复杂

(4)未来面向服务的架构(SOA)

构建一个更加内聚、松耦合、易于维护和扩展的系统

大话Chrome浏览器原理

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节点的变化情况,会作为微任务添加到队列中宏任务微任务队列中,当任务执行完成后检查当前任务的微任务队列是否存在微任务,有就取出来执行。
  • 通过PromiseMutationObserver监控某个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。
  • 只存在客户端,默认不参与与服务端的通信。
  • 接口封装。

sessionStoragelocalStorage有一个本质的区别,那就是前者只是会话级别的存储,并不是持久化存储。会话结束,也就是页面关闭,这部分sessionStorage就不复存在了。

(4)IndexedDB

IndexedDB是运行在浏览器中的非关系型数据库, 本质上是数据库,理论上这个容量是没有上限的。支持事务和二进制存储。

  • 键值对存储,内部采用对象仓库存储方式。
  • 异步操作,数据库的读写属于IO操作,浏览器提供了异步IO支持。
  • 受到同源策略限制,无法跨域访问数据库。