这是我参与更文挑战的第1天,活动详情查看: 更文挑战
阅读浏览器工作原理(一)
浏览器组成结构
-
用户界面
除了显示请求页面的主窗口以外,其他部分都属于用户界面,包括地址栏啥的。
-
浏览器引擎
在用户界面和呈现引擎之间传送指令。
-
呈现引擎
显示请求的内容。如果请求的是html和css,他就解析并显示在屏幕上。
(这个浏览器的工作原理很长..这一篇写的只是开头介绍...后面还会详细讲这一块,就不在这里费时间了)
❓对于chrome,每个标签页都分别对应一个呈现引擎,是独立的进程。
默认情况下,呈现引擎显示html和xml和图片,也可以扩展插件来显示pdf等。
-
Ui界面后端
负责绘制基本控件(即由页面(浏览器)触发事件的部件,例如组合选择框、输入框等等。)这些控件根据浏览器不同展示出来的样子也不同,但是都公开了一些事件,在底层会调用操作系统的用户界面方法来处理。
-
网络
❓用于网络调用,比如HTTP请求。
-
Js解析器
❓用于解析和执行 Javascript代码。
-
数据存储
❓浏览器需要在硬盘上保存各种数据,被称为“网络数据库”,例如 Cookie。
cookie,localstorage,sessionstorage,userData和IndexedDB
疑问
-
每个标签页都分别对应一个呈现引擎
-
chrome如何开启线程?
我们可以,重启浏览器,打开一个隐身窗口。这个时候,点击 Chrome 浏览器右上角的“选项”菜单,选择“更多工具”子菜单,点击“任务管理器”,打开 Chrome 的任务管理器的窗口,然后看看都开了哪些进程。
可以看出浏览器从关闭状态进行启动,然后新开 1 个页面至少需要 1 个网络进程、1 个浏览器进程、1 个 GPU 进程以及 1 个渲染进程,共 4 个进程。
后续再新开标签页,浏览器、网络进程、GPU进程是共享的,不会重新启动,如果2个页面属于同一站点的话,并且从a页面中打开的b页面,那么他们也会共用一个渲染进程,否则新开一个渲染进程。
默认情况下,Chrome 会为每个 Tab 标签创建一个渲染进程。出于安全考虑,渲染进程都是运行在沙箱模式下。
-
多进程的优势?
-
防⼀个⻚⾯崩溃影响整个浏览器
-
操作系统提供了限制进程权限的方法,可以控制某些进程的权限
-
进程有⾃⼰的私有内存空间,可以拥有更多的内存
-
-
站点隔离?
通常,Chrome通常把一个标签默认为一个进程,但是如果网页之间存在共享内容,就会共享同一个进程。站点隔离是 Chrome 中最近推出的一项功能,消除共享进程,可以为每个跨网站 iframe 运行单独的渲染器进程,确保不同网站在不同的进程上。以此防止发生类似Spectre和Meltdown的攻击。
-
Spectre和Meltdown的攻击?
-
-
网络模块
域名解析?
-
chrome的DNS缓存:1分钟左右,1000条,chrome://net-internals/#dns
-
操作系统的DNS缓存:ipconfig /displaydns
-
host文件:C:\Windows\System32\drivers\etc
-
向本地配置的首选DNS服务器发起域名解析递归请求必须返回地址。
DNS服务器?
一般是电信运营商提供的,也可以使用像Google提供的DNS服务器
域名解析请求协议?
DNS同时占用UDP和TCP的53端口,进行区域传送时使用TC协议,域名解析时使用UDP协议。
有两种类型的DNS服务器:主DNS服务器和辅助DNS服务器,在一个区中,主服务器读取本机数据文件获取DNS数据,辅助DNS服务器读取主DNS服务器获取数据,他启动时,会和主服务器通信并加载数据信息,这一行为称为区域传送,使用TCP协议传输。
辅助服务器会定时向主服务器查询数据是否变动,变动了就会执行区域传送并同步数据,使用TCP的原因是同步传送的数据量较多,且TCP稳定性较高。
向DNS服务器查询域名,一般返回的内容都不超过512字节,用UDP传输即可。不用经过TCP三次握手,这样DNS服务器负载更低,响应更快。
-
DNS服务器查找自身的缓存
-
代我们的服务器发起迭代DNS解析请求
-
首先找根域(.com)的ip地址
-
找(.baidu.com)的ip地址
这个DNS地址一般就是由域名注册商提供的,像万网,新网等
-
如果查到www.baidu.com的域名地址,返回给系统内核,内核返回给浏览器。
-
-
如果查不到,操作系统就会查找NetBIOS name Cache(Network Basic Input/Output System,NetBIOS名称缓存,就存在客户端电脑中的),里面缓存了近一段时间(几分钟)和计算机成功通讯的ip地址。nbtstat -c
-
查询WINS服务器(存储NETBIOS名称和IP地址对应关系)
-
客户端广播查找
-
客户端读取LMHOSTS文件(和HOSTS文件同一个目录下,写法也一样)
TCP的3次握手?
User-Agent(一般是指浏览器)会以一个随机端口(1024 < 端口 < 65535)向服务器的WEB程序(常用的有httpd,nginx等)80端口发起TCP的连接请求。
http请求经过TCP/IP4层模型的层层封包,到达服务端,进入到网卡,然后是进入到内核的TCP/IP协议栈一层一层解封,然后到达web程序建立连接。
服务器端接收到http请求后是怎么样生成html文件?
nginx读取配置文件,匹配到对应的文件路径,向内核发起IO系统调用。内核找到文件后,从硬盘上读取到内核自身的内存空间,然后再把这个文件复制到nginx进程所在的内存空间。
在文件系统层面,内核知道要获取的文件路径/a/b/c.html之后,读取元数据区中的/的inode,找到/对应的数据块编号,找到数据块中存储的目录,于是找到a这个名称在元数据区的inode号。
读取到a的inode号之后,找到他的数据块,读取目录,得到b的inode号,找到b的数据块之后,读取目录得到c.html所在的inode,最后找到c.html对应的数据块,于是获取到完整内容。
(获取到inode号 -> 找到数据块 -> 读取目录,获取到下一个目标的inode号,直到找到文件位置)
读取到html文件之后,获取静态资源?(以chrome为例)
-
开始加载
chrome把网络资源分为:(不完全)
key 意义 MainRescouce 地址栏输入得到的页面、使用frame/iframe嵌套的页面、通过超链接点击的⻚面、表单提交后跳转的页面。 kImage 图片资源 kCSSStyleSheet css资源 kScript script资源 kFont 字体 kRaw 混合类型资源,动态资源,如ajax kSVGDocument svg资源 kXSLStyleSheet 扩展样式表语言XSLT,是一种转换语言 kLinkPrefetch HTML5页面的预读取资源 kTextTrack video的字幕 kImportResource HTML Imports,将一个HTML文件导入到其他HTML文档中,例如 <link href="import/post.html" rel="import" />
kMedia 媒体资源 kManifest HTML5 应用程序缓存资源 kMock 预留的测试类型 -
预处理请求
先生成url、http header、http body,优先级等信息。
接下来检查请求是否合法,把请求做些修改。如果检查合法性返回kAbort或者kBlock,说明资源被废弃了或者被阻止了,就不去加载了。
被block的原因可能有以下几种:
key 意义 kCSP 内容安全策略检查,减少XSS攻击。 kMixedContent Mixed Content混合内容block。 kOrigin 这个主要是svg,使用use的获取svg资源的时候必须不能跨域,如下以下资源将会被阻塞 kInspector devtools的检查器 kSubresourceFilter 子资源过滤器 kOther kNone kCSP
经过某类型资源的CSP设定检查的时候没有通过就会返回堵塞原因,然后根据要求改变请求。
如果设置了content="upgrade-insecure-requests",会改变request对象,将网页的http请求强制升级为https。
如果我们只允许加载自己域的图片的话,可以加上meta标签:content="img-src 'self',或者后端设置该响应头。
kMixedContent
在https网站请求http内容,例如加载一个http的JS脚本,容易受到中间人的攻击,如修改JS的内容,从而控制整个https的⻚面。
而图片(不使用srcset而是使用src)、音频资源即使内容被修改可能只是展示出问题,如果没有设置content="block-all-mixed-content",不会被block掉,被称为被动混合内容。
如果用户允许,则可以加载主动混合内容。但是如果⻚面设置了"block-all-mixed-content",用户设置的允许blockable资源加载的设置将会失效。
如果是嵌套页面,子页面允许加载主动混合内容,而父页面不允许加载,则不允许加载。
Origin Block
svg使用use的获取svg资源的时候必须不能跨域。如果协议、域名、端口号都一样则通过检查。需要这里和同源策略是两码事,这里的源阻塞是连请求都发不出去,而同源策略只是阻塞请求的返回结果。
-
资源优先级
计算资源加载优先级,首先每个资源都有一个默认的优先级。
优先级总共分为五级:very-high、high、medium、low、very-low,判断顺序为:MainRescource⻚面、CSS、字体这种一下子就能被看到的优先级是最高的,然后就是Script、Ajax这种,而图片、音视频的默认优先级是比较低的,最低的是prefetch预加载的资源(link,rel=prefetch)。
在设定了资源默认的优先级之后,会再对一些情况做一些调整,主要是对prefetch/preload的资源。
-
降低preload的字体的优先级
预加载字体的优先级从very-high变成high
-
降低defer/async的script的优先级
script如果是defer的话,那么它优先级会变成最低
-
⻚面底部preload的script优先级变成medium
如果是preload的script,并且如果⻚面已经加载了一张图片就认为这个script是在⻚面偏底部的位置,就把它的优先级调成medium。
资源在第一张非preload的图片前认为是early,而在后面认为是late,late的script的优先级会偏低。
prefetch:在早期浏览器中,一旦遇到script,就要下载并执行完,才会继续解析剩下的DOM。
preload:遇到script 的时候,DOM会停止构建,但是继续搜索页面加载所需的资源(img,script等)并且进行预加载,不用等到DOM再开始执行的时候才加载。
-
把同步即堵塞加载的资源的优先级调成very-high
ajax同步请求在初始化的时候会被调整成very-high
本来是hight的ajax同步请求,在最后会执行max(switch判断得到的优先级,同步请求当前的优先级),所以该请求被调整成very-high
-
-
定级之后,在发请求之前,在渲染线程被转化成Net的优先级,对应关系如下:
优先级 资源类型 HEIGHEST(very high) css/font/页面/同步请求 MEDIUM(high) js/ajax LOW(medium) manifest/页面底部preload script(缓存和预加载的script资源) LOWEST(low) img/video/audio(图片视频音频) IDLE(very low) prefetch/defer script(会阻塞线程的js资源) Net Priority是请求资源的时候使用的,是在Chrome的IO线程里面进行的,好处是如果两个⻚面请求了相同资源,有缓存就能避免重复请求。
请求资源的过程,是在IO线程进行的。渲染线程和IO线程间的通信是通过Chrome封装的Mojo框架进行的。在渲染线程会发一个消息给IO线程通知它要加载资源了。
资源加载?
有一个ScheduleRequest函数,他会判断当前资源是否能开始加载了,如果能的话就准备加载了,如果不能的话就继续把它放到pending request队列里面。
有两个地方会调用它:
-
收到来自渲染线程IPC::Mojo的请求加载资源的消息
-
另外有个LoadAnyStartablePendingRequests调用了他,该函数的逻辑是遍历pending request,每次取出优先级最高的一个请求,调用ScheduleRequest判断是否可以加载了,可以的话就拿出来运行。
有三个地方会调用它:
- 要插入body标签的时候
- 每个请求完成之后,触发加载pending requests里还未加载的请求(调用LoadAnyStartablePendingRequests)
- IO线程定时循环未完成的任务,触发加载
none delayable:优先级大于等于Medium的是不可推迟的none delayable请求。
layout-blocking:layout-blocking请求是当还没有渲染body标签,并且优先级在Medium之上的如CSS的请求。(css/font/页面)
<!DOCType html> <html> <head> <meta charset="utf-8"> <link rel="icon" href="4.png"> <img src="0.png"> <img src="1.png"> <link rel="stylesheet" href="1.css"> <link rel="stylesheet" href="2.css"> <link rel="stylesheet" href="3.css"> <link rel="stylesheet" href="4.css"> <link rel="stylesheet" href="5.css"> <link rel="stylesheet" href="6.css"> <link rel="stylesheet" href="7.css"> </head> <body> <p>hello</p> <img src="2.png"> <img src="3.png"> <img src="4.png"> <img src="5.png"> <img src="6.png"> <img src="7.png"> <img src="8.png"> <img src="9.png"> <script src="1.js"></script> <script src="2.js"></script> <script src="3.js"></script> <img src="3.png"> <script> !function(){ let xhr = new XMLHttpRequest(); xhr.open("GET", "https://baidu.com"); xhr.send(); document.write("hi"); }(); </script> <link rel="stylesheet" href="9.css"> </body> </html>
- 高优先级的资源(>=Medium)、同步请求和非http(s)的请求能够立刻加载
- 只要有一个layout blocking的资源在加载,最多只能加载一个delayable的资源,这个就解释了为什么0.png能够先加载
- 只有当layout blocking和high priority的资源加载完了,才能开始加载delayable的资源,这个就解释了为什么要等CSS加载完了才能加载其它的js/图片。
- 同时加载的delayable资源同一个域只能有6个,同一个client即同一个⻚面最多只能有10 个,否则要进行排队。
可以得出结论:
-
由于1.css到9.css这几个CSS文件是high priority或者是none delayable的,所以⻢上in flight,但是还受到了同一个域最多只能有6个的限制,所以6/7/9.css这三个进入stalled的状态
-
1.css到5.css是layout-blocking的,所以最多只能再加载一个delayable的0.png,在它相邻的1.png就得排队了
-
等到high priority和layout-blocking的资源7.css/9.css/1.js加载完了,就开始加载delayable 的资源,主要是preload的js和图片
为什么1.js是high priority的而2.js和3.js却是delayable的?
一开始是Low是因为它是推测加载的(页面底部的资源),所以是优先级比较低,但是当DOM构建到那里的时候它就不是preload的,变成正常的JS加载了,所以它的优先级变成了Medium,这个可以从has_html_body标签进行推测,而2.js要等到1.js下载和解析完,它能算是正常加载,否则还是推测加载,因此它的优先级没有得到提高。
-
-
-
Js解析器
-
scanner(词法分析器)
扫描所有的源代码->词法分析->单词流(词法单元)
Tokens 在线查看网站:esprima.org/demo/pars..…
-
Parser(解析器)
单词流->语法树,其中会分析语法错误,确定词法作用域(在哪里声明的作用域就在哪里)
var a = 2
=>大概可以看出,type为声明变量,id(标识符、变量名)为a,初始值为常量2。
AST 在线查看网站:astexplorer.net/
-
Pre-Parser(惰性解析,预解析)只解析没有被立刻执行的代码,用于确定作用域,预解析后代码开始执行,才开始进行Parser(全量解析)
function foo() { console.log('a'); function inline() { console.log(''b) } } (function bar() { console.log('c') })(); foo();
- foo:不是立即执行的,所以使用预解析,里面的inline也会被解析
- bar:立即执行,直接用Parser解析
- foo():被调用的时候,用Parser进行解析,对inline又进行一次预解析。
-
-
lgnition(解释器)
语法树->字节码(要在直译机转移之后才能成为机器码)->解释执行
将 AST 转换为字节码,然后开始逐行解释执行。
早期版本的 V8 ,并没有生成中间字节码的过程,而是将所有源码转换为了机器代码。机器代码虽然执行速度更快,但是占用内存大。
-
TurboFan(编译器)
输入字节码和一些分析数据并生成优化
当Ignition执行代码后,v8会观察代码执行情况并记录执行信息,比如执行次数,参数类型。如果一个函数被调用的次数超过设定的值,就会被标记为热点函数,将函数的字节码和执行信息发送给TurboFan,他会做出进一步优化代码的假设(如假设参数类型是数字,之后就无须检查类型了),然后直接编译为机器指令。如果后面有一次发现不是数字,就表示假设错误,要进行优化回退,还原为字节码。
-
执行js
语法分析阶段:对加载完成的代码进行语法检验,检验完成后进入预编译阶段;
预编译阶段:全局、函数预编译。该阶段会进行执行上下文的创建,包括函数名以及变量提升、建立作用域链、确定 this 指向等。
执行阶段:事件循环、将编译阶段中创建的执行上下文压入调用栈,并成为正在运行的执行上下文。代码执行结束后,将其弹出调用栈。
(很多基础知识点,可以看你不知道的js和红宝书的时候在整理)
-
Google 图解:Chrome 快是有原因的,科普浏览器架构
Inside look at modern web browser (part 1)
从Chrome源码看浏览器如何加载资源
虚拟机随谈(一):解释器,树遍历解释器,基于栈与基于寄存器,大杂烩