浏览器原理

对于前端开发来说,接触最多的就是浏览器了,我们需要学习一下浏览器的原理,有助于我们更好地理解web应用,以及如何提高和优化web的性能,在遇到问题时候可以及时定位到问题。

一、宏观下的浏览器

当我们打开一个页面,打开chrome浏览器的更多工具 -> 任务管理器,我们会发现有多个进程
首先先来粗略地总结一下进程和线程的区别:

1. 线程 VS 进程

① 一个进程就是一个程序运行的实例
比如,我们启动一个程序的时候,操作系统就会为该程序分配内存空间,用来存放该程序的代码,数据,文件和执行任务的线程。这种运行环境成为进程。

② 一个进程可以运行多个线程
当一个进程中只有一个执行任务的主线程,则成为单线程,如果有多个线程,则成为多线程,

③进程中一旦有一个线程执行出错,会导致整个进程崩溃

④ 线程之间共享进程的数据,但是进程之间的内容是相互隔离的,毫无关系。

⑤当一个进程关闭之后,操作系统会回收进程所占用的内存空间

2. 早期 vs 现代的浏览器

早期的浏览器是单进程的,所有的模块都运行到同一个进程中,这样会导致很多的问题:

  • 不稳定
    插件容易导致浏览器崩溃
  • 不流畅
    当js运行的代码,阻塞了页面,比如遇到了死循环,由于在同一个进程,容易导致失去响应;
    页面存在内存泄露的可能,使内存占用变高,导致卡顿。
  • 不安全
    页面运行的插件,可以获取操作系统的任意资源,如果是恶意的插件,则有可能放出病毒,盗取账号密码等。
    现代的浏览器是多进程的,一个页面就是进程,不同站点的不同页面的数据是分隔开的;根剧之前早期的缺陷来看看是如何解决的:
    当页面崩溃时影响的只是当前的进程,不会导致整个浏览器崩溃;
    js是运行在渲染进程中,所以只会影响到当前的进程,不会影响其他页面;当关闭一个页面时候,整个渲染进程也会被关闭,占用的内存会被系统回收,就不存在内存泄露的问题。
    对于安全问题,多进程可以使用安全沙箱,把插件进程和渲染进程锁在沙箱中,不允许访问涉及安全问题的位置。

3. 多进程浏览器

最新的chrome浏览器包括:

  • 浏览器主进程
    负责界面显示,用户交互,管理子进程,管理存储;
  • 渲染进程
    负责将HTML,CSS,JS转化为用户可交互的页面,排版引擎blink和v8引擎都是运行在该进程中的,每创建一个tab标签就会创建一个渲染进程;
  • GPU进程
    最早用于3D CSS效果,后来UI界面和网页都采用GPU绘画,就成为一个必须的进程;
  • 网络进程
    负责加载网络资源
  • 插件进程
    负责插件的运行
    衡量web页面性能的一个指标是FP,即从页面加载到首次绘制的时长,影响这个指标最重要的因素是网络加载速度,我们需要了解TCP/IP协议,众所周知,两方要通信,则必须要建立起同一的标准和规则,协议就这么来了。
    这边简单描述一下是如何通信的:

4. TCP/IP 通信

发送端在应用层中生成http报文数据,传送到传输层,传输层则会在报文头部加入TCP首部,再传送到网络层,加上远程服务器的地址和源地址的IP,即IP头,再经过数据链路层,加入以太网首部,接收端则会一步步解析,最终解析出客户端的请求内容。
TCP特点:

  • 对于数据包丢失,提供重传机制
  • 引入数据包排序机制,将大文件分割成许多小文件,最后根据序号组成一个大文件
    http协议是建立在TCP连接的基础上,通常是由浏览器发起的请求,当我们在浏览器输入一个地址到一个页面渲染完成,究竟发生了什么呢?

5. 浏览器端HTTP 请求流程

当我们在浏览器的地址栏输入一个页面的地址时
①首先是请求行,例如:

GET /test.html HTTP1.1

②再查找缓存
如果请求的页面存在于浏览器的缓存中,则不需要发起真正的请求,这样做的好处是可以减轻服务端的压力,使加载速度更快。如果缓存中没有,则继续
③获取IP和端口号
我们知道要请求一个页面的资源,必须要知道这个资源所在的IP,端口号,和对应的文件目录等等。浏览器会根据输入的域名,请求DNS返回域名对应的IP,如果某个域名曾经解析过,则可以直接从DNS数据缓存中取数据,而不用再次去请求,端口号可以从url中获取或者默认的80端口。
HTTP和TCP的关系:
HTTP的内容是通过TCP传输数据来实现的,所以,http网络请求的第一步就是和TCP建立连接。
④建立连接
chrome机制规定同一个域名同时最多只能建立6个TCP连接,需要排队等待,所以当IP和端口都准备好了,不一定马上就可以建立连接。建立TCP连接需要经过三次握手,详细的就不再说明了,建立完后之后,浏览器就可以换和服务器进行通信了。
⑤发送数据
通过TCP数据传输,将http请求行发送到服务器端,如果是POST方法,则需要加上请求体,一般是一些参数数据。请求行发送之后,还要发送请求体,主要包括浏览器的基础信息,比如内核,操作系统,浏览器内核,cookie或者token等等。
⑥返回数据
服务器解析数据包之后,返回响应行(HTTP/1.1 200 ok),响应头和响应体。响应行中带有协议版本,状态码。
⑦断开和完成
当客户端收到了响应信息,就要关闭TCP连接,如果浏览器在请求头中加入Connect: keep-alive则会保持连接装填,可以节省下次连接建立需要的事件,提升加载速度。
到目前为止,一次http请求就算完成啦!
但是有个问题,就是当第一次请求时候比较缓慢 ,第二次再请求时候速度就变快了很多。这是为啥呢?那就要来看看浏览器的缓存机制了

6. 浏览器中页面缓存

从以上的分析,我们可以知道缓存主要是DNS缓存和页面资源的缓存

  • DNS缓存
    浏览器本地把对应的IP和域名关联起来
  • 页面资源缓存
    当浏览器第一次发起http请求,并在请求头中的Cache-control设置max-age即缓存过期时,
    Cache-Control:Max-age=2000
    
    第一次请求缓存是空的,就直接访问服务器,然后缓存到本地,当再次请求时,会判断是否在缓存期内,如果在则直接返回本地缓存,如果超过缓存期,会继续发起网络请求,并在请求头中带上:
    If-None-Match:"xxx"
    
    服务器会根据这个值来判断请求的资源是否有更新,如果没有更新,则告诉浏览器当前的缓存可以用,返回304,否则就直接返回最新的资源

7. cookie和token

在实际的业务场景中,我们访问网页,需要保存用户的登录状态,除非用户主动退出,那么浏览器是怎么做到的呢?主要有两种技术:

  • cookie
    当用户输入账号密码登录时候,服务器会验证账号密码,成功则在响应头中加上:
    Set-Cookie: UTD=xxx
    
    浏览器在收到响应头时,会解析响应头,发现含有Set-cookie,则会把这个属性对应的值保存到本地,再下次请求时候,会把这个值写到请求头的Cookie里
    Cookie: UTD=xxx
    
    服务器收到这个字段时,回去session表中查找对应的cookie值(sessionID),并找到用户的登录的状态,并识别身份,再将生成的数据发送给浏览器。当用户登出,session在客户端和服务器端都会被销毁。
  • token
    当用户输入账号密码登录时候,服务器会生成token,并返回给客户端,客户端需要在下次请求时请求头中带上:
    Authorization header: Bear xxxxx;
    
    服务端根据对应的token就可以获取用户的信息。

区别:
cookie的验证是有状态的,绘画需要一直在服务端和客户端保持,一旦登出,则全部销毁;
token的验证是无状态的,token存储在客户端,在请求时候根据约定,放在Authorization header,服务端只需要验证token是否有效,不需要验证是否是登录状态,一旦登出,token在客户端可以销毁,但是不影响服务端。
但是现在普遍是使用token而不用cookie,优势在于:
①后端不需要保持对token的记录,cookie则需要,每个token都是独立的,只需要验证有效性,提高了效率
②token的CORS可以很好地解决跨域问题

二、JS工作机制

1. 变量的提升

在js代码执行过程中,js把变量和函数的声明提升到代码开头并设置默认值undefined的行为称之为变量提升,且函数首先会被提升,接着才是变量。
举个例子:

console.log(foo);   
function foo(){
    console.log("函数声明");
}
var foo = "变量";

此处打印的是函数
等价于:

function foo(){
    console.log("函数声明");
}
var foo;
console.log(foo);// 打印函数,重新定义的foo会被忽略   
foo = "变量";
console.log(foo);  // 打印变量

那么为什么要先变量提升呢?
在js代码执行过程中,要先经过编译阶段,编译后,会生成执行上下文可执行代码,执行上下文存在一个变量环境对象,保存了变量提升的内容。最后在执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。输出结果。那么什么是执行上下文呢?

执行上下文是 JavaScript 执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,确定该函数在执行期间用到的诸如 this、变量、对象以及函数等。

2. 调用栈

调用栈是用来管理函数调用关系的一种数据结构
比如在全局作用域中声明一个函数,并且调用了,则会生成一个全局执行上下文,还有一个函数执行上下文,js引擎通过调用栈来管理这些数据。
但是我们在实际开发过程中,可能会遇到栈溢出,那是因为调用栈的大小是有限制的,超过一定数目就会报错
例如:

function division(a,b){
    return division(a,b)
}
console.log(division(1,2))

当调用division时,并为其创建执行上下文,压入栈,但是这个函数是递归的,就会一直创建新的调用栈

3. 作用域

作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
我们知道使用ES6语法的let和const就不会有变量提升的问题看,不知道你是否有疑惑,js是如何做到对块级作用域的支持呢?

在一个函数中,用let或者const声明的变量会被放在函数执行上下文的词法环境中,如果在函数中又声明了新的作用域块,则会将块中let和const声明的变量压入词法环境栈中,执行之后才会被弹出栈。

4. 作用域链

在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个称为outer,当一段代码使用了一个变量的时候,如果在当前的变量环境中没有查到,则会从outer指向的执行上下文中查找,我们就把这个查找称为作用域链。

5.词法作用域

词法作用域指作用域由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过他能够预测代码在执行过程中如何查找标识符。
词法作用域在代码级阶段就已经决定好了,和函数调用方式无关。

function bar() {
    var myName = "hello"
    let test1 = 100
    if (1) {
        let myName = "浏览器"
        console.log(test)
    }
}
function foo() {
    var myName = "a"
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myName = "a"
let myAge = 10
let test = 1
foo() // 1

分析:调用foo时,间接调用了bar,当时bar执行上下文的outer指向全局执行上下文,所以在bar中找不到test变量时,就会从全局变量中查找,此时发现在全局执行上下文的词法环境中找到了test,则输出值。

6. 闭包

根据词法作用域的规则,我们知道内部函数可以访问外部函数声明的变量,当通过调用外部函数返回内部函数,则外部函数已经执行结束,在调用栈中弹出执行上下文,但是内部函数引用了外部函数中的变量,这些变量依然保存在内存中,我们把这些变量的集合成为闭包。

function foo() {
    var myName = "a"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("b")
bar.getName()
console.log(bar.getName())

那么闭包是还说呢么时候可能被回收呢?
如果引用闭包的函数是个全局变量,那么闭包会一直存在直到页面关闭,如果这个闭包以后不再使用的话,就会造成内存泄露。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次js引擎执行垃圾回收时,判断闭包不再使用时就会回收这块内存。
使用闭包注意点:
如果闭包会一直使用,那么可以作为变量而存在,如果使用频率且占用较大内存,则尽量让它成为一个局部变量。

7. this

this是和执行上下文绑定的,全局执行上下文中的this指向windows对象,这是this和作用域链的唯一交点。
有三种方式来设置函数执行上下文中的this:
(1)可以通过call,apply,bind来改变this的指向
(2)通过对象调用方法设置,this指向的是调用的对象
(3)通过构造函数设置,this指向新建的对象
总结:

  • 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
  • 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。

三、V8工作原理

1. javascript内存机制

在了解内存机制之前,我们先区分一下几个概念:
(1)静态语言和动态语言
我们把在使用之前就需要确认变量的数据类型成为静态语言,反之成为动态语言。
例如C、C++、Java为静态语言,js为动态语言。
(2)强语言和弱语言
当一个变量被声明成了一个类型,但是可以被其他类型的变量赋值,隐式转换类型。我们把这种成称为弱类型语言,反之为强语言类型。
比如python,java为强语言类型,js,php, c, c++为弱语言类型。

内存空间

在js执行过程中,主要有三种类型内存空间,分别是:
① 代码空间
② 栈空间: 维护执行上下文的状态,存储原始类型数据
③ 堆空间:存储引用类型
那么,为什么要区分使用栈空间和堆空间呢?
我们知道在js执行过程中,会产生执行上下文,当一个函数执行完毕时,它的执行上下文会被弹出栈从而被回收。通常情况下,栈空间不会设置得太大,是用来存放一些原型类型的小数据,相对的,引用类型占据的空间都比较大,所以可以放到堆中。但是缺点是:分配内存和回收内存都会占用一定时间。
那么我们可以根据堆和栈进一步分析闭包变量保存方式:
当内部函数引用了外部变量时,会形成闭包,并把内部函数引用的外部变量保存到堆中,形成了闭包,在下次调用内部函数时,就直接去堆中取变量。
关于引用类型的浅拷贝和深拷贝方式(8.浅拷贝和深拷贝)

2. 垃圾回收

我们知道在代码执行过程中,有些数据被使用之后,可能就不再需要,为了节省内存的占用,就出现了垃圾数据回收,分为手动和自动垃圾回收,例如C/C++可以手动垃圾回收,js可以自动垃圾回收。
我们知道js中的数据存在栈和堆中,那么这两种内存空间的垃圾如何回收呢?

  • 调用栈中的数据回收

在调用栈中有执行上下文,当执行到某个函数时,会有一个记录当前执行状态的指针(ESP),指向调用栈中函数的执行上下文,当函数调用结束时,ESP会下移到下一个执行上下文,那么这个下移操作就是销毁函数执行上下文的过程。原来的内存已经无效,随时都会被其他内容覆盖。

  • 堆中的数据

在说明之前,我们先来了解一下代际假说(The Generational Hypothesis)
(1)大部分的对象在内存中存在的时间很短,也就是说已经分配,很快就不用了。
(2)还有的对象则会存活很久
所以在V8中把堆分为新生代老生代
新生代存放生存时间短的对象,容量比较小,为1~8M,由副垃圾回收
老生代存放生存时间长的对象,容量比较大,由主垃圾回收
垃圾回收器的通用流程:
① 标记活动对象和非活动对象
② 回收非活动对象所占的内存
③ 清除内存后,会产生碎片内存,需要做内存整理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,192评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,858评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,517评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,148评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,162评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,905评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,537评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,439评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,956评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,083评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,218评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,899评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,565评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,093评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,201评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,539评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,215评论 2 358

推荐阅读更多精彩内容

  • 一、了解浏览器的意义 如果不了解浏览器的运行原理,浏览器对于我们来说就是一个黑盒,所以我们的代码最终运行效果怎样、...
    丁乐ya阅读 292评论 0 0
  • 浏览器架构 用户界面 主进程 内核渲染引擎JS 引擎执行栈事件触发线程消息队列微任务宏任务网络异步线程定时器线程 ...
    菊花泡茶阅读 883评论 0 1
  • 前端必读:浏览器内部工作原理[https://kb.cnblogs.com/page/129756/] 作者: T...
    我是强强阅读 1,149评论 0 2
  • 进程与线程 进程是内存分配的最小单位,线程是CPU调度的最小单位 浏览器内核——浏览器渲染进程 GUI渲染线程解析...
    如沐春风ei阅读 321评论 0 0
  • 摘要:区块链经历5到10年的发展,我们也将迎来 Web 3.0 和整个DApp生态的大发展、大繁荣,届时DApp浏...
    笔名辉哥阅读 5,758评论 2 51