浏览器底层
今天记录一下新学到的知识,浏览器底层原理,这是一条故事线,我们从双击谷歌浏览器开始。
- 打开浏览器后,操作系统分配内存给浏览器。
- 浏览器拥有多个进程
浏览器进程:
1 Brower 浏览器自身的一些功能
2 GPU 处理不同tab的内容渲染到统一显示区域
3 Renderer tab页内的网页展示,js代码运行在此进程
4 Plugin 控制网页使用的所有插件 - 我们没开启一个tab就会创建一个Renderer进程,如何验证?
我们同时按住shift + esc 打开任务管理器
从图中可以看到百度和知乎有不同的进程ID,我们思考一下浏览器这样设计的意义在哪里?当然设计者有自己的想法,我简单聊一聊我们浏览器的同源策略。我们从进程开始说起,我们在操作系统的书上可以了解到进程是资源分配的最小单位,资源有很多了,比如磁盘,内存等等,这时候我们来关注内存,可以从上面这句话知道,每一个进程之间的内存是相互独立的,并且每一个进程中的线程是共享内存的。如果知乎和百度在同一进程中,我们在百度的tab上通过网络请求回来的数据,知乎也能够读取到,这样就违背了我们的同源策略,所以每一个tab会开启不同的进程。浏览器为了节约内存,也会把同源的但是不同tab的分到一个进程里面。(这一段是我自己结合然后理解的,欢迎大家的指正)
我们继续浏览器打开的流程
- 我们重点关注Renderer进程,这就是我们所看到的网页区域。它拥有自己的五个线程。这里就是我们面试经常遇到的问题,从url到显示完成。
1 GUI
2 JavaScript引擎
3 定时触发器
4 事件触发
5 异步http请求
1.GUI渲染线程
主要负责页面渲染,解析HTML,CSS,构建DOM树,布局,绘制。重绘回流也会执行该线程,与JS引擎线程互斥,遇到JS代码需要执行时,GUI会被挂起知道任务队列为空。
2.JS引擎线程
负责处理和执行JS脚本。因为互斥关系,JS不能够堵塞,不能页面会卡顿。这里也会出现一个事件队列的问题,我们的代码有同步任务和异步任务,我们都知道同步任务优先级高于异步任务,除开宏任务中的同步任务,微任务高于异步任务。
- 事件队列,EventLoop
当我们在解析HTML的时候,遇到JS脚本,首先看是否拥有defer和async属性。这里的执行阶段与DOM解析阶段一定是互斥的,但是加载不用互斥,因为加载是浏览器进程完成的。
1 如果没有,阻塞GUI,开始加载并执行JS脚本;
2 拥有async,异步加载与执行JS脚本;
3 defer同样异步加载,但是会在DOM元素解析完成之后执行JS脚本。
引用一张别人大神的图片
说回事件队列和事件循环,在执行JS阶段的时候,这时候会出现同步任务和异步任务(ajax,
微任务:promise,process.nextTick(node))
宏任务:setTimeout,setInterval,I/O,setImmediate(node)
遇到异步任务之后会将该任务添加到异步队列中并不会立即执行。待全部的同步任务执行完毕之后,来到异步任务队列,这个时候就会根据宏任务和微任务来判断哪一个任务优先执行。在每次执行完宏任务之后,会去清空微任务。
会优先执行微任务,然后在执行宏任务。执行完之后GUI线程渲染。这个时候会不断的去事件队列取出任务,直到为空为止。
一定要自己深入去想,事件队列是跟事件有关系的,是指的在那同一刻是按照这样的顺序执行。
这里我们还能看到一个ajax,我们查不到他的资料,他到底是宏还是微?我们做一个思考,ajax的请求和响应是有时间的1.5个RTT的三次握手,然后传输数据,会耗时,这个时候JS引擎执行宏微时,任务队列并没有把其添加进去。不太理解这个地方,需要查阅更多资料。
3.定时器线程
负责处理异步定时器,JS引擎遇到定时器后,交给该线程,计数完毕后将回调任务添加到事件队列末尾等待JS执行。
4.事件触发线程
将准备好的事件交给JS引擎执行,定时器回调,用户触发的事件,ajax请求的回调。
5.异步http请求线程
负责异步请求,promise,ajax等,异步请求返回结果时,将回调函数加入到任务队列末尾,等待JS执行。
接下来我们来完成面试官的考题相信大家就会很清楚了,url地址输入后,首先要做的就是把域名变成ip地址,我们会先到浏览器缓存中查看是否有该域名对应的资源。这个地方又会涉及到一个新的问题就是浏览器缓存,通常指的是协商缓存和强缓存,强缓存会在上一次响应头中检测到cahe-control:max-age=31536000,public,immutable,根据时间判断此次的请求是否使用该缓存,如果超时,就会下一步协商缓存。如果是协商缓存响应头
etag: ‘5c20abbd-e2e8’
last-modified: Mon, 24 Dec 2018 09:49:49 GMT
,这时候就会去host文件中查找域名和ip地址的对应关系,如果没有就本地DNS服务器解析,如果也没有就去更外层的DNS服务器解析,解析完成之后我们拿到ip地址就可以发起http请求了(首先三次握手,http请求响应,四次挥手)。根据http版本的不同我们会有不同的传输方式,http1.0是短链接,一般来说,一次http请求响应就会经历三次握手四次挥手,http1.1改进了加入了keep-live机制,在约定下传输的时间或者传输http个数,时间或者个数一到就会四次挥手。http2 有更大的改观,使用了头部压缩,二进制帧传输,多路复用的技术,让请求响应速度加快。我们拿到对应的html,css,js之后,我们的浏览器开始了渲染工作,从解析html,css,到遇到js脚本后挂起,执行完js后在继续解析(可能会导致重绘,回流),解析完成之后,就要利用GUI线程渲染我们的页面了,但是我们页面可能使用了z-index属性,这个时候,GPU进程就会帮助我们叠加这些不同的渲染层,让我们看到最终的效果。如果没有请求了,http连接也会断开。
终于来到最后一个部分了,内存的分配
我们来看看JS和汇编的联系。
我们大部分人都知道JS有8种类型,7种基本类型(undefined,null,Number,String,Boolean,Symbol,BigInt),1中引用类型(Object),也都知道基本类型的数据存在于栈区,引用类型的地址存在栈区,内容存在堆区中,那我们的函数他是如何存储的呢?它肯定是引用类型,用函数名 instanceof Object 得到true
在汇编语言中,我们的内存主要分为
代码段:执行代码的一块内存
数据段:静态内存,已初始化的变量
BSS段:静态内存,未初始化的变量
堆:动态分配的内存段,大小不定
栈:存放局部变量
那我们的JS怎么办呢?前面说过,Renderer进程有有一块自己的内存,我们知道JS引擎是在Renderer进程中的一个线程,那这个JS引擎就会分配和使用这一块内存,就会用来存储我们的变量,代码执行等等。和汇编书中讲的还是有一些差别。
主要分为:
- ECstack 执行环境栈,负责代码的执行区域。
- EC 执行上下文,区分不同的作用域
- VO 存储变量的空间,全局上下文
- AO 存储变量的空间,私有上下文
在这里就会产生一个问题,如果我在自己的作用域中并没有找到改变量怎么办?我们就会根据作用域向外部一层一层查找直到找到为止,这就是作用域链。
写不完了,要吃饭了。吃完继续写。