本文摘自哔哩哔哩UP主:OBJTUBE的卢克儿
互联网是如何运作的
第一当我们访问一个网站时
当你的电脑连接互联网之后,你的电脑将会分配一个编码地址,这个地址我没称为IP地址,当你访问一个网站的时候,也就是你在访问他的服务器,此时你要访问一个网页的index.html 那么我们把你的电脑称为客户端,在远程服务的称为服务端,这么面向客户的程序 C/S 架构。
我们的网线其实就是连接两者的桥梁,将二进制转换为电信号,在服务端转换成二进制,这时候我们就需要通过 TCP/IP协议,TCP/IP 协议分为嗄层,应用层,TCP传输控制层,IP网络层,链路层,通过分层来明确各自的工作职责,通过定义明确的接口来协同工作,每一层3都可以试用下层的功能。
- 应用层:提供特定于应用程序的协议 HTTP FTP
- 传输控制层:发送数据包到计算机上试用的特定端口的应用程序
- 网络层:使用IP地址将数据包发送的特定的计算机
- 链路层:将二进制数据包于网络层信号相互转换
TCP:面向连接的可靠字节流服务协议(必须经过三次握手,四次挥手)
IP:是不可靠的,只负责你查询到指定主机,然后传输数据
其实IP指的是协议,并不是指的是IP地址,IPV4 目前已经使用完毕,现在使用的是IPV6
其实真的一次访问是这样的
大家都听过宽带猫,光猫这个东西的,客户端发送的数据通过宽带猫将数字信号转换为电信号,在公共的网络进行传输,公共网络通过连接I网络服务提供商(ISP)也就是生活中常见的电信,联通,移动等,数据包通过电话网络和ISP之后他们将路由到ISP主干网络,数据包通常会经过多个路由器,并经过多个主干网络知道找到目的地,互联网主干网有许多相互连接的大型网络组成,这些大型网络称为网络服务的提供商,简称NSP,NSP是为ISP提供网络主干服务的公司。ISP可以向NSP哪里批量购入带宽,为客户提供网络接入服务,NSP通过网络访问点NAP相连,来交互数据包,每个NSP都必须连接到至少三个网络访问点,当然真正的会更复杂,在互联网中会有一个特殊的计算器-路由器,路由器上会有个路由表,记录其子网络的所有IP地址,然而他并不知道上次网络所包含的IP地址,当数据包进入到路由器,路由器检查路由表上是否有目的地的IP地址,如果有则直接发送给那个网络,否则就向上继续寻找,直到NSP主干网为止,然后在向下找目的地。
当然我们不会去记住每一个IP地址,所以就有了域名,那么这里就要使用到域名服务器(DNS)DNS是一个分布式数据库,上面记住了域名和其IP地址的对应关系,在浏览器输入网址时,浏览器首先连接DNS服务器,获取到域名的IP地址后,浏览器在连接该IP的服务器
浏览器是如何运行的
当浏览器获取网页数据之后是如何转换为视图的呢
浏览器结构图
- 用户界面
- 浏览器引擎 (数据持久层)
- 渲染引擎 (网络,JS解释器)
浏览器是运行在操作系统上的一个应用程序,每一个应用程序必须至少启动一个进程来执行其功能,那么进程就会创建一些线程去帮助他执行些小的功能
进程是操作系统进行资源分配和调度的基本单位,可以申请和拥有计算机资源,进程是程序的基本执行体
线程是操作系统能够进行运行调度的最小单位,一个进程可以并发多个线程,每条线程并执行不同的任务
当我们启动摸一个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存,当应用关闭时,该内存空间就会被回收,进程可以启动更多的进程来执行任务,由于每一个进程分配的空间是独立的,如果两个进程需要传递某一些数据,则需要通过进程间通信管道IPC来传递,很多应用程序都是多进程的结构,这样是为了避免某一个进程卡死,由于进程间相互独立,这样就不会影响到整个应用程序。
进程可以将任务拆分更多细小的任务吗,然后通过创建多个线程并执行不同的任务,同一个进程下线程是可以共享数据的
浏览器是多进程的程序,再找在早期的浏览器中是单进程的程序,
一个进程大概有页面线程负责页面 JS线程执行JS代码等等,单进程的结构引发了很多问题,一个标签页卡死(线程)就会导致整个程序卡死。
二是不安全,浏览器直接是可以共享数据的,那JS线程岂不是可以随意范围浏览器内的所有数据
三是不流畅
现在都是多线程的
其中浏览器进程负责控制浏览器标签页外的用户界面(地址栏,书签,前进后退按钮)
网络进程负责发起接受网络请求
GPU进程负责整个浏览器页面渲染
插件进程负责整个浏览器使用的插件(并不是指的是市场里面的插件 而是 例如flash动画)
渲染器进程用来控制现实tab标签内的所有内容
浏览器有可能会为每一个标签页常见一个进程 ,浏览器有四种方案,当然最安全的就是一个tab一个进程这是最安全的,最常见的问题就是,当一个死循环会把整个浏览器跑奔溃
那么我说说浏览器的工作流程把
当你在浏览器导航栏输入网址的时候,浏览器的UI线程会捕捉你的输入内容,如果访问的是网址,则UI线程会启动一个网络线程来请求DNS进行域名解析,接着来开始连接服务器获取数据,如果你的输入不是一串网址二十一串关键字,浏览器就知道你是要搜索,于是就是使用默认的搜索配置来查询
当网络线程获取到数据包之后,会通过safeBrowsing来检查站点是否时恶意站点,如果是,则会弹出提示和警告,但依然可以选择访问。safeBrowsing是谷歌的安全检测系统,网络线程会通知UI线程,然后UI线程会创建渲染进程(render Thread)来渲染界面
浏览器进程通过IPC管道将数据传递给渲染进程,接受到的数据也就是HTML,渲染器进程的核心任务就是把HTML CSS JS IMAGE等资源渲染成用户可以交互的web界面,渲染器进程的主线程将HTML进行解析,构造DOM树结构,DOM也就是文档对象模型,是浏览器对页面在其内部的表现形式,是web开发程序员可以通过JS与之交互的数据结构和API, HTML首先通过tokeniser标记化,通过词法分析将输入的HTML内容解析成多个标记,根据识别后的标记进行DOM树构造,在DOM树构造的过程中会创建document对象,然后以document的为根节点的DOM树不断进行修改,向其中添加各种元素.HTML代码中往往会引入一些额外的资源(图片,css,js脚本等),图片和css这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为他们不会影响DOM的生成,但当HTML标签解析过程中遇到script标签,就会停止html的解析流程,转而去加载解析并执行JS代码
这是因为浏览器并不知道JS执行是否会改变页面的HTML结构,如果JS代码里面用了document.write来修改html,那之前的HTML解析就没有意义了,这也就是为什么我们再说要把script标签放入合适的位置,或者使用async或者defer属性来异步加载和执行js代码
在HTML解析完成后,我们就会获得一个DOM Tree (树),但我们还不知道树上的每一个节点长什么样子,主线程需要解析CSS,并确定每一个DOM节点的计算样式,即使你没有提供自定义的css样式,浏览器也会有自己的默认样式表
知道样式之后,我们就需要每一个节点需要放置的位置,占多大区域,这个阶段被称为layout布局
主线程通过遍历dom和计算好的样式来生成layout tree ,layout tree 上每一个节点都记录了x,y坐标和边框的大小,这里需要注意的一点是DOM tree 和 layout tree 并不是一一对应的,设置了display: none 不会出现在layout tree 上面,而在before 伪类中添加了content 值的元素,content的内容会出现在layout tree 上,不会出现DOM树中,这是因为DOM是通过HTML解析获取的,并不关系样式,而layout tree 是根据DOM和计算好的样式来生成的,layout tree 是和最后展示在屏幕 的节点对应的
现在知道的元素的大小,形状,样式,位置,这还不够,我们还需要知道它是以什么样的顺序绘制的(paint) 举例来说,z-index这个属性会影响节点绘制的层级关系,如果我们按照DOM的层级结构来绘制界面,则会导致错误的渲染.所以北路保证绘制的层级,主线程遍历layout tree 创建一个绘制记录表,该表记录了绘制的顺序,这个阶段被称为绘制.然后把这些都现实在屏幕上被称为栅格化.
主线程遍历layout tree layout tree 生成完毕和绘制顺序确定后,主线程将这些信息传递给合成器(生成界面),合成器线程将每一个图层栅格化,有可能一层可能像界面的整个长度一样大,因此合成器将他们切分为很多图块,然后将每一个土块发送发给栅格化线程,栅格线程栅格化每一个图块,并将他们存储在GPU内存中,将土块栅格化完成之后,合成器线程将收集图块信息,这些信息记录了图块在内存中的位置何在页面中那个位置绘制图块信息
根据这些信息信息合成器线程生成一个合成器帧,然后这个合成器帧通过IPC传递给六拉你去进程,接着浏览器进程将合成器合成帧传给GPU,然后GPU在渲染到屏幕上.这时候就完成了现实
当你滚动屏幕,都会生成一个新的合成器帧,新的帧在传递给GPU,然后在渲染在屏幕中
当我们改变了一个元素的尺寸位置属性时,会重新进行样式计算,布局,绘制已经后面的所有流程,这种行为称为重排
当我们改变莫一个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这种行为称为重绘
重排和重绘都会展占用主线程,我们的JS代码也是运行在主线程的,这样就会出现抢占执行时间的问题
如果有一个不断重排重绘的动画,浏览器则需要在每一帧都运行样式,计算布局和绘制的操作,当页面以每秒60帧的刷新率才不会让用户感觉到卡顿,如果在运行动画时候还有大量的js代码任务需要执行,因为布局绘制和JS执行都是在主线程上面运行的,当在一帧的时间内布局绘制结束后还有剩余的事件就会去执行js代码,如果js执行时间过长,就会导致在下一帧开始的时候js还没执行完,没有及时归还主线程的使用权,导致下一帧动画没有按时渲染,就会出现动画的卡顿.
解决方案可以通过requestAnimationFrame()吗,这个方法会在会在每一帧被调用,把js分成更小的任务块(分到每一帧)在每一帧时间用前暂停JS执行归还主线程.这样的话,在下一帧开始前归还主线程使用权,主线程就可以按时执行布局和绘制
当然还有css中的transform,这是在合成器线程和栅格线程中完成的,是不会占用主线程的,通过该属性的动画不会经过布局和绘制,二十直接运行在合成器线程和栅格线程中,所以不会收到主线程的影响,更重要的是transform实现动画不需要布局绘制,样式计算等操作,所以节省了很多计算时间(位置变化,宽高变化(旋转,3D等))这些都是可以使用transform代替
JavaScript运行原理
JavaScript虽然是动态语言,但他执行起来依然非常块,尤其是在启动时,比如用node运行一段代码,几乎是瞬间完成,这是因为现代的JavaScript引擎都使用了一项技术just in time compilation 运行时编译,简称JIT,JIT就是在运行阶段生成机器代码,而不是提前生成,JIT把代码的运行和生成机器的代码是结合在一起的,在运行的时候收集类型信息,然后根据这些信息编译生成机器码之后,之后在运行这些代码时,就直接使用生成好的机器代码
哪还有另外一种方式叫AOT,就是在运行前提前生成好机器代码(C++)
JavaScript被CPU执行前,需要通过某种程序将js转换成低级的机器语言并执行,这种程序被称为JavaScript引擎
比如chrome使用的V8引擎, webkit使用的JavaScriptCore等等
源码通过解析器成抽象语法AST然后通过解释器转成字节码,字节码与平台无关,能够在不同平台上运行,字节码最终通过编译器生成机器码,由于不同的处理器平台使用的机器代码会有差异,所以编译器会根据当前的平台来编译出相对于的机器代码(汇编代码)
Javascript V8引擎
V8引擎是一个接受JavaScript代码编译代码然后执行代码的C++程序,编译后的代码可以在多种操作系统多种处理器上运行
V8主要负责
- 编译和执行js代码
- 处理调用栈
- 内存的配置,垃圾的回收