admin管理员组

文章数量:1530056

在前端岗位的面试中,经常会出现这样两个问题:

  • 主流浏览器有哪些?他们的内核分别是什么?
  • 描述一下从 浏览器地址栏输入url到页面显示的过程?

事实上,关于这两个问题的答案,网上遍地都是,但如果不深入了解其中奥妙,只是背个答案,那么很有可能面试官提出的一个相关问题就会把你问倒。例如,浏览器有几个进程,几个线程,他们之间是如何分配的 进程和线程之间的关系 事件循环是什么 等等,所以还是得好好深入了解了解,才能在面试时,“妥妥的”~

并且,个人认为理解浏览器内核对于前端开发人员非常重要,因为不同的内核可能会对网页的解释和渲染产生影响,从而影响页面的布局、样式和交互效果。在开发过程中,需要考虑不同浏览器内核的差异,并进行兼容性测试和优化,以确保网页在不同浏览器上都能正确显示和运行

首先第一个问题:什么样的浏览器才能被称为主流浏览器?

拥有自己独立内核的浏览器才能被称为主流浏览器

例如下面这些浏览器,括号中的是它们的内核

  • Navigator (Gecko)

  • Opera(以前是Preston -》 现在是Blink)

  • IE(Trident) 现在的IE已经替换成了Edge(Chromium)

  • FireFox(Gecko)

  • Safari(Webkit)

  • Chrome(Webkit =》 Chromium =》 blink)

那么下一个问题是:什么是浏览器内核?

浏览器内核是通过取得页面内容,整理信息,计算和组合最终输出可视化的图像结果,通常也被视为浏览器渲染进程。例如在Chrome浏览器中,会为每个Tab页面单独启用进程,因此每个tab网页都有其独立的渲染引擎实例,有些渲染进程会被浏览器自己的优化机制进行合并

我们常说的浏览器内核,它其实是浏览器渲染进程,其内部又包含了 GUI渲染线程(也就是常说的渲染引擎)、JS解析引擎线程(JavaScript引擎),事件触发线程、定时器触发线程以及异步网络请求线程

那在解释他们之前,先来简单了解一下进程和线程:

  • 进程是资源分配的最小单位,线程是系统执行的最小单位

  • 进程是系统内存分配的一小部分内存空间

  • 进程之间相互独立(不同进程之间可以相互通信,不过代价很大)

  • 进程由单个或多个线程组成

  • 多个线程之间是可以相互协作完成工作的

  • 同一个进程中各个线程之间共享同一块内存空间

浏览器使用的是多进程多线程模式

因为现在的网页复杂性非常高。如果整个浏览器是单进程的,有可能某个tab页的抛错就会导致整个浏览器的崩溃。同时多个界面互相可以访问相同的内存和相同的执行环境,安全性也是一个大的问题,所以浏览器需要采用多进程模式

  • 浏览器主进程(Browser进程):有且只有一个,控制chrome的地址栏,书签栏、返回和前进按钮,同时还有浏览器界面上的不可见的部分,例如网络请求和文件访问
  • 第三方插件进程:每种插件对应创建一个进程,但只有该插件运行时才会创建
  • GPU进程:主要用于3D渲染
  • 浏览器渲染进程:内部是多线程的,负责页面渲染,脚本执行,事件处理等等
    • GUI渲染线程:用来解析 html css
    • js解析引擎线程: 它是一个单线程,用来执行js脚本
    • 事件触发线程:事件队列、事件循环
    • 定时器触发线程: setTimeout()等等
    • 异步网络请求线程: AJAX

下面来了解了解 浏览器内核中这些线程各自的作用

  • GUI线程(渲染引擎)

负责渲染浏览器界面,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行,当界面需要重绘或由于某种操作引发的回流(reflow)时,该线程就会执行。

渲染引擎负责解析网页的 HTML、XML、图像等内容,并将其转换为可视化的网页形式展示给用户。它负责处理网页的布局、样式计算、绘制等任务。不同浏览器的内核对网页的解释和渲染方式可能会有差异,因此不同浏览器的渲染效果也会有所不同

  • WebKit:主要用于 Safari 和 Chrome 浏览器。
  • Gecko:主要用于 Firefox 浏览器。
  • Trident:主要用于旧版本的 Internet Explorer 浏览器。
  • Blink:基于 WebKit,用于 Chrome、Opera 和部分 Chromium 浏览器
  • js引擎线程

也称为JS内核,负责处理JavaScript脚本程序,JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS线程在运行JS程序

JavaScript 引擎负责解析和执行网页中的 JavaScript 代码,实现网页的动态交互和功能。不同浏览器的 JavaScript 引擎性能和特性也可能存在差异

  • 常见的 JavaScript 引擎包括:
    • V8:用于 Chrome 和 Opera 浏览器,具有高性能和快速执行速度。
    • SpiderMonkey:用于 Firefox 浏览器。
    • JavaScriptCore:用于 Safari 浏览器。
  • 定时触发器线程

传说中的setInterval与setTimeout所在的线程, 计数线程

  • 事件触发线程

属于浏览器而不是JS引擎,当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中。当对应的事件符合触发条件被触发时,该线程会把是事件添加到待处理队列的队尾,等待JS引擎的处理。

  • 异步http请求线程

XMLHttpRequest在连接后是通过浏览器新开的一个线程请求。当检测到状态更新时,如果没有设置回调函数,异步线程就产生状态 变更事件,将这个回调再放入事件队列中,等待JS引擎执行。

需要注意的是,GUI线程 与 js引擎线程 是互斥的,由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JavaScript线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JavaScript引擎为互斥的关系,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行

从浏览器地址栏输入url后 直至渲染的整个过程

如果从简单来说,这个过程如下:

  • 浏览器根据请求的URL交给DNS域名解析,找到真实IP,向服务器发起请求;
  • 服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图象等);
  • 浏览器对加载到的资源(HTML、JS、CSS等)进行语法解析,建立相应的内部数据结构(如HTMLDOM);
  • 载入解析到的资源文件,渲染页面,完成

可这其中包含了许多的细节,仅仅只背这几行文字 对这个问题的深入程度 远远不够

详细步骤如下:

(1)在浏览器地址栏输入URL

(2)浏览器会根据用户输入的url去查看缓存,如果请求资源在缓存中并且未过期,就跳转到转码步骤

  • 如果资源未缓存,发起新请求

  • 如果已缓存,检验是否过期,如果没有过期则直接提供给客户端,否则与服务器进行验证。

    • 检验新鲜通常有两个HTTP头进行控制 Expires 和 Cache-Control
      • HTTP1.0提供Expires,值为一个绝对时间表示缓存新鲜日期
      • HTTP1.1增加了Cache-Control: max-age=,值为以秒为单位的最大新鲜时间

打开浏览器的控制台,在请求头的参数列表中就可以找到它们


(3)浏览器解析URL获取协议,主机,端口,path

URL(uniform resource locator,统一资源定位符)用于定位网络服务

URL是一个固定格式的字符串

它表达了:

从网络中哪台计算机(domain)中的哪个程序(port)寻找哪个服务(path),并注明了获取服务的具体细节(path),以及要用什么样的协议通信(schema)

这里面包含了一些细节:

  • 当协议是http端口为80时,端口可以省略
  • 当协议是https端口为443时,端口可以省略
  • schemadomainpath是必填的,其他的根据具体的要求填写

(4)浏览器组装一个HTTP(GET)请求报文,这个请求报文需要包含 请求行 + 请求头 + 请求体 三个部分

(5)浏览器 获取主机ip地址的过程如下,依次向下去查找

  • 浏览器缓存

  • 本机缓存

  • hosts文件

  • 路由器缓存

  • ISP DNS缓存

  • DNS递归查询(可能存在负载均衡导致每次IP不一样)

(6)找到主机的ip的地址后,准备与服务端建立连接,下面以TCP传输协议为例,打开一个socket与目标IP地址端口建立TCP链接

,三次握手的详细过程如下:

  • 第一次握手
    客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。
  • 第二次握手
    服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
  • 第三次握手
    客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。

注意:我们上面写的ack和ACK,不是同一个概念:

  • 小写的ack代表的是头部的确认号Acknowledge number, 缩写ack,是对上一个包的序号进行确认的号,ack=seq+1。
  • 大写的ACK,则是我们上面说的TCP首部的标志位,用于标志的TCP包是否对上一个包进行了确认操作,如果确认了,则把ACK标志位设置成1

(7)TCP链接建立后 客户端发送HTTP请求

(8)服务器接受请求并解析,将请求转发到服务程序,如虚拟主机使用HTTP Host头部判断请求的服务程序

(9)服务器检查HTTP请求头是否包含缓存验证信息如果验证缓存新鲜,返回304等对应状态码

(10)处理程序读取完整请求并准备HTTP响应,可能需要查询数据库等操作

(11)服务器将响应报文通过TCP连接发送回浏览器

(12)浏览器接收HTTP响应,然后根据情况选择关闭TCP连接或者保留重用,关闭TCP连接的四次握手的详细过程如下:

  • 第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
  • 第二次挥手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
  • 第三次挥手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
  • 第四次挥手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。

在这一部分还存在一些问题待思考:为什么连接只需要三次,而断开的时候却需要四次握手?如果在握手或者挥手过程中出现丢包,是否会导致脏数据的出现? 这些读者可自行查查资料

(13)浏览器检查响应状态吗:是否为1XX,3XX, 4XX, 5XX,这些情况处理与2XX不同

(14)如果资源可缓存,进行缓存

浏览器在接收到服务器响应后,会检查响应中的缓存相关头部信息,如"Cache-Control"、"Expires"和"ETag"等,根据缓存头部信息,浏览器决定是否将该资源存储在缓存中

(15)对响应进行解码(例如gzip压缩)

(16)根据资源类型决定如何处理(假设资源为HTML文档)

(17)解析HTML文档,构件DOM树,下载资源,构造CSSOM树,执行js脚本,这些操作没有严格的先后顺序,以下分别解释

  • 构建DOM树

    • Tokenizing:词法分析过程,根据HTML规范将字符流解析为标记
    • Lexing:语法分析过程,将上一步词法分析得到的结果转换为对象并定义属性和规则
    • DOM construction:根据HTML标记关系将对象组成DOM树

    如果在解析过程中遇到图片、样式表、js文件,则启动下载

  • 构建CSSOM树:

    • Tokenizing:词法分析过程,字符流转换为标记流
    • Node:根据标记创建节点
    • CSSOM:节点创建CSSOM树
  • 根据DOM树和CSSOM树构建渲染树

    • 从DOM树的根节点遍历所有可见节点

      • 不可见节点包括:script,meta这样本身不可见的标签 或者是 被css隐藏的节点,如display: none
    • 对每一个可见节点,找到恰当的CSSOM规则并应用

    • 发布可视节点的内容和计算样式

  • js解析如下:

    • 浏览器创建Document对象并解析HTML,将解析到的元素和文本节点添加到文档中,此时document.readystate为loading

    • HTML解析器遇到没有async和defer的script时,将他们添加到文档中,然后执行行内或外部脚本。这些脚本会同步执行,并且在脚本下载和执行时解析器会暂停。这样就可以用document.write()把文本插入到输入流中。同步脚本经常简单定义函数和注册事件处理程序,他们可以遍历和操作script和他们之前的文档内容

    • 当解析器遇到设置了async属性的script时,开始下载脚本并继续解析文档。脚本会在它下载完成后尽快执行,但是解析器不会停下来等它下载。异步脚本禁止使用document.write(),它们可以访问自己script和之前的文档元素

    • 当文档完成解析,document.readState变成interactive

    • 所有defer脚本会按照在文档出现的顺序执行,延迟脚本能访问完整文档树,禁止使用document.write()

    • 浏览器在Document对象上触发DOMContentLoaded事件

    • 此时文档完全解析完成,浏览器可能还在等待如图片等内容加载,等这些内容完成载入并且所有异步脚本完成载入和执行,document.readState变为complete,window触发load事件

(18)显示页面(HTML解析过程中会逐步显示页面)

以上就是对这两个问题的详细解释,希望能对读者有所帮助,在这两个问题中,参杂了许多的其他的知识,比如 进程线程、计算机网络相关的传输协议TCP、UDP,应用层协议http、https,常见的http状态码及其含义,js引擎中的事件循环机制等等,需要读者根据自己不清楚的地方再去深入了解了解

本文标签: 浏览器内核地址栏过程页面