(借鉴了极客时间的《浏览器工作原理及其实践》,若需要删除请联系2560720865@qq.com)
进程:是程序的一个运行实例。
线程:不能单独存在,依附在进程上的一个完成任务的执行路径。
两者的关系:一个进程可以包含多个线程,一个线程只能服务于一个进程。
多线程模式,极大的提高了浏览器对任务的处理速度。
特点:任何一个线程都是不可或缺的,如果出错,整个进程都会随之崩溃。(2)对于同进程的不同线程,可以共享进程的数据(3)当一个进程关闭之后,操作系统会回收进程所占用的内存(4)进程之间的内容相互隔离,以免因为一个页面崩溃而影响其他不相干的页面崩溃
目前 Chrome 浏览器进程
浏览器主进程:主要管控页面显示,用户交互等,还有存储功能。
网络进程:负责页面的网络资源加载等。
GPU 进程:绘制页面。起初是为了绘制 3D CSS
渲染进程:将 HTML、CSS、JavaScript 等代码转化为用户可以交互的页面。该进程是在沙箱模式下进行的,为了安全。
插件进程:负责插件的运行。
PS:沙箱模式:给运行程序外面套一个盒子,不影响其正常运行,但是会阻止其不安全行为,例如在硬盘上随意的写入任何数据,读取敏感位置的数据等。早期的浏览器插件可以用 c\c++来写,因此可以访问系统下任何资源并且操作,具有极大的安全隐患。
我认为,推动浏览器发展的主要动力:安全性,流畅度,轻便程度。主要是这三个方向
要满足传递数据,那么必须符合网络协议(简称 IP)。数据是以数据包的形式传递,而且如果数据包较大,那么会分散成很多小的数据包,进行传递。
访问网站的实质:一台计算机向另一台计算机请求信息。
IP 地址:一台计算机的地址。在 A 向 B 发送数据包时,数据包会加上 IP 头,以存储 A 的 IP 地址(数据包发送的地方)和 B 的 IP 地址(数据包到达的地方)等其他信息。
如果还需要精确,比如送到哪个程序上,那么需要基于基于 IP 之上开发能和应用打交道的协议,常见的是用户数据包协议,简称 UDP(User Datagram Protocol),其最重要的信息是端口号。
IP 通过 IP 地址信息把数据包发送给指定的电脑,而 UDP 通过端口号把数据包分发给正确的程序。就类似快递,IP 是哪个地址,而 UDP 是决定给哪个人。
缺点:在使用 UDP 发送数据时,有各种因素会导致数据包出错,虽然 UDP 可以校验数据是否正确,但是对于错误的数据包,UDP 并不提供重发机制,只是丢弃当前的包,而且 UDP 在发送之后也无法知道是否能达到目的地。UDP 不能保证数据可靠性,但是传输速度却非常快。
大文件会被拆分成很多小的数据包来传输,这些小的数据包会经过不同的路由,并在不同的时间到达接收端,而 UDP 协议并不知道如何组装这些数据包,从而把这些数据包还原成完整的文件。
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议,对于数据包丢失的情况,TCP 提供重传机制;此外,TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件。
建立在 TCP 协议之上的一种允许浏览器向服务器获取资源的协议,是 Web 的基础。同样也是浏览器使用最为广泛的协议。HTTP 的内容是通过 TCP 的传输数据阶段来实现
域名和 IP 地址有着一一对应的关系,是通过域名系统(Domain Name System,即 DNS)来进行建立的关系。所以在访问域名的时候通过 DNS 获取到域名对应的 IP 地址,进而传输数据。DNS 还有缓存功能,即访问过的网站,在下次查询的时候直接使用而不是再去发起一个网络请求
有时候输入网址,不加 www.开头也能进入相应的网址,是因为重定向。
二次页面打开很快,主要原因是第一次加载页面过程中,缓存了一些耗时的数据。
DNS 缓存和页面资源缓存。
浏览器中的 HTTP 请求从发起到结束一共经历了如下八个阶段:构建请求、查找缓存、准备 IP 和端口、等待 TCP 队列、建立 TCP 连接、发起 HTTP 请求、服务器处理请求、服务器返回请求和断开连接。
用户输入:1.输入判定是 URL 还是搜索内容 2.URL 请求过程:重定向(可能有) 响应数据类型处理(判断是文本类型 text/html 还是下载类型 application/octet-stream) 3.准备渲染进程(同一域名下会在一个渲染进程里面) 4.提交文档(更新前进后退状态,更新安全状态,更新 URL 地址,更新 Web 页面) 5.渲染阶段
重定向可能有是因为,如果本身协议是 https 但是输入只是写了 http 那么就会重定向,然后返回真正需要到的域名在 location 里面
此外,如果多个页面的根域名相同,那么使用的是一个渲染进程
1.DOM 生成(需要构建 DOM 树,浏览器无法直接理解和使用 HTML) 2.样式计算(1.将 CSS 转化为 styleSheets 2.标准化属性值比如 2em 转化为 32px 3.计算 DOM 树中每个节点 的样式,样式是会从父节点继承的) 3.布局(生成布局树,也就是真正需要展示的结构,比如有些结构有display:none那么布局树里面就不会生成该节点 再进行布局计算,计算出每个节点的坐标位置) 4.分层(渲染引擎需要为特点节点生成专用的图层,并且生成一颗对应的图层树,比如有层叠上下文(z-index,明确定位,滤镜属性 filter:blur 透明度 opacity)的就需要,需要剪裁(clip,比如说超出显示区域就隐藏)的地方也会创建图层) 5.图层绘制(会将一个个图层的绘制拆分为很多小指令,按着绘制列表进行绘制) 6.栅格化(绘制列表由主线程到合成线程之后,合成线程将图层划分为图块,合成线程会按照视口附近的图块(最小的栅格化单位)来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图,渲染进程维护了一个栅格化的线程池) 7.发送命令(一旦所有图块都被光栅化,合成线程会生成绘制图块的命令 DrawQuad 给浏览器进程)8.生成页面并显示(viz 组件然后根据该命令将页面内容绘制在内存中,最后再将内存显示在屏幕上)
渲染流程里面,在通过构建 DOM 树和计算层叠样式表之后,根据这两个,创建布局树,如果某个节点存在 display:none,那么根本在布局树中找到该节点。如果是 visible:hidden 那么还是会在布局树中找到,只不过在后面图层绘制里面不进行绘制。
修改常规流中元素的 display 通常会造成文档重排,这是因为元素的消失和出现会影响到页面的布局。而修改 visibility 属性只会造成本元素的重绘,即元素的视觉表现发生变化,但不会影响到页面的布局
对于 var,和函数,他们都会提升到当前作用域的顶部
对于 JS 而言并不是严格按照,代码的声明顺序执行的

代码执行之前会先编译,编译时,var 声明的变量和函数会放在环境变量中,然后代码执行时,再从环境变量中找相应的代码
每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
ES6 之前不支持块级作用域,那么就有个弊端,var 声明的变量会提升到当前作用域的顶部,那么预想和实际的输出就不一致了
执行 test 函数后,先查看是否有 var 声明和函数声明,有的化,会提升到顶部。
那么实际上是
那么打印出来是undefined
还有类似于下面
var 创建的会提升到当前作用域顶部,那么就没有消失
因此在ES6就引出了 const 和 let 还有块级作用域,那么在块级作用域生成的 let 和 const 不会影响外部的代码。
在编译阶段,通过 var 声明的内部变量是在变量环境,let 和 const 声明的在词法环境。
词法环境是一个小型的栈,当该作用域完成时,这个栈就清空,那么在使用该变量时,会沿着一条路径进行查找
这样子去查找
作用域链:按着位置进行查找声明变量的链式结构,从局部上下文查找到全局上下文为止。
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
那么比如
这里就跟词法作用域有关。
第一段代码,test2 是在 test1 里面声明的。然后那么 test2 的outer(即外部引用)指向 test1 的执行上下文,如果找不到,再取到 test1 的 outer。
第二段代码,test2 是在全局执行上下文中声明的,那么他的 outer 指向全局执行上下文。
闭包:
这是一段闭包代码。根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量
那么当 foo 函数执行完成后本应该销毁的内容,因为 getName 和 setName 而保留下来从而形成了闭包
成了这种情况。
闭包的定义:在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包
闭包通常是直到页面关闭才消失
闭包:当会一直使用,作为全局闭包最好;如果使用频率不高而且占用内存大,那么作为局部闭包最好,这样当这个局部销毁时,闭包也会跟着销毁
或者像上面那个闭包,让 bar = null,那么 foo 闭包没有指向它的东西,就会被回收。
这段代码就不会产生闭包,bar.printName 声明不在 foo 内部,那么根据词法作用域,foo 里面的变量根本不会保存,因此不会产生闭包,那么其打印结果映射,极客邦*2
在 Javascript 的对象中调用的方法要使用对象中的属性需要使用到 this,指向该实例,this 和执行上下文有关系,上下文分为,全局上下文,函数执行上下文和 eval 执行上下文
全局上下文的 this 指向 window 对象
在默认情况下,调用函数的 this 指向的时全局对象(浏览器中是 window,NodeJs 里面是 globalThis)
可以通过 call 进行设置 this 指向,而且 call 是直接执行
foo.call(bar),将 foo 的 this 指向变为 bar,并且执行。那么 bar 的 myName 被更改,bind 和 apply 也相类似
bind()会创建一个新的函数,新函数被调用时,bind 的第一个参数作为 this(不会立即执行)
apply()方法会调用一个具有给定this值的函数,并以一个数组(或类似数组对象)作为函数的参数。,apply()需要一个对象作为第一个参数来设置函数的this值,然后提供一个数组作为函数的参数。
使用对象来调用其中的一个方法,该方法指向对象本身的。但是如果使用全局变量,将对象中的方法赋值给它,那么在调用它的时候其内部的 this 指向全局变量
比如obj.print()那么在 print 里面打印 this,this 指向的是对象的,实际上 Javascript 引擎在执行obj.print()时默认的转化为了obj.print().call(obj)
但是如果改为箭头函数,那么就是打印{}箭头函数没有 this,那么他会套用上一层的 this 指向
这里面的 this 指向 createObj 的实例对象
this 的缺陷:嵌套函数的 this 不能继承,普通函数的 this 指向全局对象
这里面 bar 的 this 指向 window,而 showThis 的 this 指向对象。
改进方法:在 bar 外面声明 self 赋值为 this,然后让里面不再调用 this 而是 self。
2.使用箭头函数,箭头函数没有 this,那么当使用时,会沿着作用域链向外部查找,直到找到 this
在运行过曾在需要检测数据类型的语言称为动态语言,JavaScript 就是一个动态语言。
同时 JavaScript 也是弱类型语言(支持隐式转化的)。
Boolean,Null,Undefined,Number,BigInt,String,Symbol 这几种属于基本类型,声明时存储在栈中
Object 属于引用类型,声明时存储在堆当中,栈中存储了相应的地址。Array 是特殊的 Object
之间闭包那里,是调用函数之后产生了闭包对象
深拷贝数组或者对象:
1.利用 JSON 字符串的转化
2.递归处理
或者这样子简化一下
如果遇到循环引用
比如上述情况就需要处理无限递归
当函数执行上下文被销毁时,里面的内存会被垃圾回收机制自动回收。在调用栈里面存在,存在当前执行状态的指针(ESP),当某函数执行完成,ESP 就会下移,因此销毁之前函数执行上下文。
这就是栈里面的垃圾回收
堆里面的垃圾数据,需要用到 Javascript 中的垃圾回收器
待际假说:第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;第二个是不死的对象,会活得更久。
在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
新生代存储空间较小,通常只有 1-8M,老生代空间就大得多。
V8 使用不同垃圾回收器,新生代使用副垃圾回收器,老生代使用主垃圾回收器
垃圾回收器的工作流程:标记活动对象与非活动对象(不再使用的对象,如何判定不再使用,那么就看是否有地址指向这片空间,如果没有那么就算是非活动对象)
2.统一回收非活动对象的内存。
3.做好内存管理。回收后,都是内存碎片,不连续。如果需要使用连续的大内存这样就出现了问题,因此需要做好内存管理。But有些回收器不会产生内存碎片。
一般比较小的对象是分配到新生区
新生代用 Scavenge 算法处理,将其分为对象区域和空闲区域。新加入的对象,分配到对象区域,当对象区域快满时,就会执行垃圾回收操作。当标记完成后,副垃圾回收器会将存活的对象复制到空闲区域,并且有序的进行排列,这就完成了内存管理,复制后空闲区域没有内存碎片,然后再将对象空间和空闲空间进行翻转。经过两次垃圾清理后仍然存活的对象就会晋升到老生区
一般比较大的对象会直接分配到老生区。因此老生区对象要不是比较大,要不就是存活时间长。因为对象可能比较大,如果使用 Scavenge 算法效率较低,因此使用的标记-清除算法。
标记从一组根元素开始,递归遍历,能达到的元素为活动对象,遍历整个不能达到为非活动对象。(遍历整个调用栈去查找)
标记整理算法,是标记完先不清楚,先整理再进行清楚操作
全停顿:Javascript 执行垃圾回收算法时 Javascript 的脚本会停下来,等待回收之后再去执行。
为了降低老生代的垃圾回收造成的卡顿,V8 会将标记过程分为一个个子标记过程,让标记过程和 Javascript 应用逻辑交替运行,直至标记完成阶段,这样的是增量标记
编译器和解释器
类似 c++类型的就是编译型语言,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了
而 JavaScript 这样的是解释性语言在每次运行时都需要通过解释器对程序进行动态解释和执行

V8 将 JavaScript 代码生成 AST 和上下文
先分词,即词法分析,将一行行源码拆解为 token,比如 var 算一个 token。再解析,即语法分析,通过上面的 token 生成语法 AST
开始时,没有字节码,都是使用的机器码,但是在手机上占用内存太高,然后重构了架构,变成了使用字节码,字节码需要通过解释器解释之后变为机器码然后被执行
当解释器 Ignition 发现了热点代码(一段代码被重复执行多次),那么 TurboFan 就会直接将其转变为机器码,以提升代码效率
字节码配合解释器和编译器这种技术叫做及时编译(JIT)
想要在线程运行过程中,能接收并执行新的任务,就需要事件循环机制
渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息
消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列
为了保证效率和实时性,将 DOM 更新添加到当前宏任务中的微任务队列
微任务是:一种异步任务,包括 Promise 的回调、MutationObserver 的回调(检测 DOM 的变化)等,微任务会在当前宏任务结束后,下一个宏任务执行前执行。
优势:控制流程(可以在未来任意时间操作),异步操作(不会阻塞主线程),优先级调度(微任务具有更高的优先级),同步执行(紧接着宏任务执行),控制并发(可以使用 Promise.all()确保完成所有微任务之后再执行下一个宏任务)
调用 setTimeout 设置回调函数,渲染进程将会创建一个回调任务,包含了回调函数 showName、当前发起时间、延迟执行时间。消息队列中有 ProcessDelayTask。
id 就是使用定时器返回的 id,以便清除它。
setTimeout 嵌套使用超过五次以上,那么系统设置最短时间间隔为 4ms
未激活的页面,setTimeout 执行最小间隔是 100ms
延迟执行时间最大值:2147483647 毫秒,大于时溢出,相当于延时设置为 0
setTimeout 设置的回调函数使用 this 不太好,但是如果需要有两种方法:
1.将使用对象的方法放入匿名函数中
2.使用 bind 进行绑定
XMLHttpRequest 实现了从 Web 服务器获取数据的能力,得到数据后再通过 DOM 操作更新页面内容,这样就能实现页面更新而且不打扰用户
回调函数 callback 是在主函数 doWork 返回之前执行的,我们把这个回调过程称为同步回调。回调函数在主函数外部执行的过程称为异步回调。
代码运行:
1.创建 xhr 对象let xhr = new XMLHttpRequest()
2.注册回调函数xhr.open('GET',地址)响应成功请求xhr.addEventListener('loadend',回调函数)
xhr.ontimeout = ()=>{}和xhr.onerror = ()=>{}
3.配置参数:xhr.tiemout = 3000 xhr.responseType = 'text' xhr.setRequestHeader('X_TEST','time.geekbang')
设置 xhr 的超时时间 设置响应的返回格式
4.返回数据xhr.send()
宏任务:位于消息队列中的任务,setTimeout,用户交互事件,文件读写等操作。
微任务:微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
V8 引擎会创建微任务队列。主要有两种:·一使用 MutationObserver 监视某个 DOM 节点,然后使用 JavaScript 修改这个节点时,触发的回调是微任务。
·二:Promise,当调用 Promise.resolve()或者 Promise.reject()时也会添加微任务。(但是 let t = new Promise()是立即执行的,那么内部如果有同步语句就会立即执行)
WHATWG 规范把执行微任务的时间点称为检查点


MutationObserver 采用异步+微任务,异步保证同步操作带来的性能问题,微任务保证了更新节点的实时性
页面编程的一大特点:异步回调。
太多回调函数的存在会导致代码逻辑不连贯,不线性。
我们可以将异步回调的过程封装,比如封装 XMLHttpRequest 的请求过程进行封装。但是如果业务很复杂的情况下,就会遇到回调地狱问题(多层嵌套,代码逻辑不够清晰),回调地狱问题导致代码杂乱不堪,其一就是因为嵌套调用,此外,还有就是任务的不确定性,有成功有失败,如果有 n 个请求,那么就有 2^n 情况,非常复杂。
因此,需要消除多层嵌套以及合并多个任务的错误处理。
Promise 可以解决这个问题
Promise 可以将多层嵌套变为链式调用,因为 promise.then 是返回的一个新的 Promise 对象。
Promise 实现了回调函数的延时绑定,需要将回调函数 onResolve 的返回值穿透到最外层
最后无论在哪出现了错误,是最终的 catch 去执行,这样就解决了每个任务都需要单独处理异常的情况。Promise 对象的错误具有“冒泡”的性质,会一直像后传递,直到被 onReject 或者 catch 捕捉到为止。
当一个 Promise 的操作失败时,它会抛出一个异常,这个异常可以通过 Promise 链中的.catch()方法和.then()方法的错误处理函数来捕获和处理。
在 Promise 链中,如果一个 Promise 失败了,它后面的 Promise 的.catch()方法或者.then()方法的错误处理函数就会被执行,并且接收到上一个 Promise 抛出的异常。
执行 resolve 函数,会触发 demo.then 设置的回调函数 onResolve,可以猜测 resolve 内部调用了通过 demo.then 设置的 onResolve 函数。
Promise 为什么要使用微任务是由 Promise 回调函数延迟绑定技术导致的,因为使用定时器的话效率太差。(而且希望给他更高的优先级)
Promise 实现返回值穿透:通过 then 实现,then 有两个参数,第一个参数是成功回调参数,第二个是错误回调参数
Promise 出错后,“冒泡”传递给最后捕捉异常的函数是通过链式调用实现的。
当 Promise 的一个状态变为 rejected,他会立即调用它上面的所有拒绝回调函数,错误原因就像冒泡一样沿着 Promise 链上传递
手写 Promise(来自掘金社区)
当请求资源,请求完毕又开始请求使用 Promise 也比较麻烦(太多的 then 函数),因此在ES7引入了async和await,实现了在不阻塞主线程的情况下,使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰
带星号函数,而且是可以暂停执行和恢复执行的;
具体实现:1.在生成器函数内部执行一段代码,如果遇到 yield 关键字,JavaScript 引擎会返回关键字后面的内容并且暂停执行函数
2.外部可以使用 next 方法恢复函数的执行。
协程是比线程更加轻量的东西,一个线程上可以存在多个协程,但是同时只能执行一个协程。
如果从 A 协程启动 B 协程,那么 A 协程就是 B 协程的父协程
上面那段代码和下面这段代码非常类似
对于上面代码的执行过程:1.先声明了 gen 协程,然后进入到生成器函数里面,当第一个 fetch 完成后,将得到的 response 响应返回给父协程,因此父协程执行了第一个.then,那么就会打印response1和返回的东西(fetch完成后返回的),然后调用 getGenPromise,继续到 gen 协议里面,打印response111和undefined,因为yield将值返回给父协程了,然后在重复操作,等待第二个 fetch 完成,返回给父协程,然后父协程执行第二个 then,打印response2和fetch得到的东西,因为在这个.then 里面没有调用 getGenPromise(即执行 gen.next()这个东西),因此不会打印response2222
async:是一个通过异步执行,并且隐示返回Promise作为结果的函数
await:等待一个 Promise,然后 await 返回 Promise 的解析值,如果 await 后面的是一个立即执行函数,那么会影响后面代码的执行
await 后面的如果是函数会等待该函数执行完成,然后再执行下面的,如果不是那么就会以new Promise((resolve,reject)=>{resovle(某值)})的形式调用
因此
这个先打印0然后执行 foo,foo 是 async 声明的,最终会返回 Promise 对象,然后打印 1。因为存在 await 它等待的是一个 Promise 对象,而 await 后面是一个值,执行 new Promise,将这 100 包裹在 其中的 resolve 中,await 需要等待 Promise.then 的一个返回,或者抛出错误,因此返回到主协程打印3,清空微队列打印100,然后执行 await 后面的语句,打印2
先打印script start然后调用 bar,打印bar start,等待 foo 函数的执行,foo 函数可以立即执行,因此打印foo,因为 foo 是立即执行函数,且是 async 声明的,因此 await 后面的是Promise{undefined},因为是一个 Promise 对象会有 then 方法,那么就返回主协程,执行 new Promise 打印promise executor然后将 then 中的函数放入微队列,然后打印script end宏任务结束后立即执行微任务队列中的,因此打印bar end和promise then最后执行下一个宏任务,打印setTimeout
DOM,由网络进程中得到的 HTML 在渲染进程中无法理解,因此将 HTML 字节流转化为 DOM 结构
作用:1.是页面生成的基本 2.JavaScript 操作 HTML 的接口 3.将不安全的内容拒之门外。
渲染器内部存在HTML解释器,作用:将 HTML 渲染成 DOM 结构。HTML 解析器是网络进程加载了多少数据,就解析多少数据。
当网络进程接收到响应头时,根据content-type字段判断类型,如果是text/html那么就会判定为 HTML 文件,然后为其创建一个渲染进程,并且在他俩之间建立一个共享数据的管道。
一:通过分词器分词,分为 Token
二、三阶段:将 Token 转化为 DOM 节点,并将 DOM 节点,添加到 DOM 树中。
如果压入栈中的是 StartTag Token,HTMLParser 为其创建一个 DOM 节点,并将其加入到 DOM 树中。
如果解析出的是文本 Token,会创建一个文本节点,并且不会压入栈中父节点就为当前栈顶的 DOM 节点。
如果解析出来是 EndTag 标签,HTMLParser 会查看当前栈顶是否是对应的 StartTag,如果是,那么就弹出栈顶,以表示该 div 元素解析完成
当解析到 script 标签时,渲染引擎会判定为脚本,然后暂停对 DOM 的解析。JavaScript 文件的下载过程会阻塞 DOM 解析,Chrome 做了一个优化,有预解析操作。
接受到 HTML 字节流会开启预解析线程,遇到 JavaScript 和 CSS 文件,预解析线程会提前下载这些数据
如果 JavaScript 文件中没有操作 DOM 相关代码,可以将该 JavaScript 脚本设置为异步加载,通过async或者defer来标记代码。
例如
两者类似,但是有不同。async 标志的脚本文件一旦加载完成就会立即执行,但是是异步加载脚本,而且不会阻塞页面的解析过程。而使用了 defer 标记的脚本文件,需要在 DOMContentLoaded 事件(所有 DOM 解析完成后会触发的事件)之前执行。async属性用于异步加载脚本,执行时间不确定
渲染引擎遇到 JavaScript 脚本时,不管该脚本是否操纵了 CSSOM,都会执行 CSS 文件下载,解析操作,再执行 JavaScript 脚本

为什么需要 CSSOM,因为浏览器也不能直接解析 CSS 层叠样式表,化为 CSSOM 能便于渲染引擎解析。而且还有两个作用:一,提供给 JavaScript 操作样式表的能力,第二个是为布局树的合成提供基础的样式信息。CSSOM 是在document.styleSheets
渲染流水线影响到了首次页面展示的速度,而首次页面展示的速度又直接影响到了用户体验。
解析白屏:提交数据后渲染进程会创建一个空白页面,会等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树。
通常瓶颈主要体现在:下载 CSS,JavaScript 和执行 JavaScript
那么为了缩短白屏时间:1.内联 JavaScript 和 css。
2.通过 webpack 等工具移除一些不必要的注释,并且压缩 JavaScript
3.还可以将一些不需要在解析 HTML 阶段使用 async 和 defer
4.大的 CSS 文件,可以通过媒体查询属性,拆分为多个不同用途的 CSS 文件
如果一个屏幕的刷新频率是 60hz,那么每秒固定读取 60 次前显卡的缓冲区的图像。
显卡作用:合成图像,并且将图像保存到后缓冲区,并且一旦写到后缓冲区,系统就会让前缓冲区和后缓冲区互换。
帧:每一张图片。帧率:每秒钟更新了多少帧
任意一帧的生成方式有重排、重绘和合成三种方式,通常渲染路径越长,生成图像花费的时间越多。重排:需要根据 CSSOM 和 DOM 计算布局树,整个渲染流水线的每个阶段都执行一遍,如果布局复杂,那么就很难保证渲染的效率。重绘:没有重新布局的阶段,但是仍然需要重新计算绘制信息。合成:不需要布局和绘制,而且如果使用了 GPU,合成效率会非常高。
因此对于生成一帧图像优先使用合成,如果不行在退求其次使用重绘或重排。
为什么需要分层:每次页面有很小的改变,如果没有分层,都会触发重排或者重绘机制,非常影响页面的渲染效率。
类似于 photoShop,多层图片合成为一个网页。将素材分解为多个图层的操作称为分层,将图层合并到一起的操作就是合成。
考虑到一个页面被划分为两个层,当进行到下一帧的渲染时,上面的一帧可能需要实现某些变换,如平移、旋转、缩放、阴影或者 Alpha 渐变,这时候合成器只需要将两个层进行相应的变化操作就可以了,显卡处理这些操作驾轻就熟,所以这个合成过程时间非常短。
合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。
分层从宏观上提升渲染效率,分块则是从微观提示渲染效率。合成线程会将每个图层分割为大小固定的图块,加速页面的显示速度
chrome 在首次合成图块的时候使用一个低分辨率的图片。
如何利用分层技术优化代码,可以在 css 代码中使用 will-change
提前告诉渲染引擎,这时候渲染引擎会将该元素单独实现一帧,等这些变换发生时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就大大提升了渲染的效率。这也是 CSS 动画比 JavaScript 动画高效的原因
页面优化:其实就是让页面更快地显示和响应
页面生存周期有三个阶段:加载阶段、交互阶段和关闭阶段

能阻塞网页首次渲染的资源为关键资源
影响首次渲染的主要因素:关键资源的个数,关键资源的大小,请求关键资源需要多少 RRT(Round Trip Time,往返时延)。总的优化原则就是减少关键资源个数,降低关键资源大小,降低关键资源的 RRT 次数。
优化:1.减少 JS 脚本执行时间。·一次执行的函数分解为多个任务。·另一种采用 Web Workers
2.避免强制同步布局(添加 DOM 节点之后又获取信息) 3.避免布局抖动(大量重复插入或者删除节点)
4.合理利用 CSS 动画,CSS 是在合成线程上完成动画的,因此不会造成主线程卡顿
5.避免频繁的垃圾回收
往 DOM 上频繁添加或者删除一个节点的话,会引起大量的重排(样式计算、布局、绘制、栅格化、合成等任务),还可能重绘或者合成,造成大量的性能消耗。此外,不当操作可能会引发强制同步布局和布局抖动。
为了减少对DOM的操作次数,因此就诞生了虚拟 DOM
虚拟 DOM:
·将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上。
·变化被应用到虚拟 DOM 上时,虚拟 DOM 不会急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了
·虚拟 DOM 收集到足够的变化时,再把这些变化一次性应用到真实的 DOM

React Fiber 更新机制,当有数据更新就会有一个新的虚拟 DOM,然后和老的进行递归对比,找出变化节点并且将变化节点应用到 DOM 上。
核心算法是:reconciliation,当虚拟 DOM 比较复杂时,可能会造成主线程卡顿,因此重写了 Fiber reconciler 之前的较 Stack reconclier
这里叫 Fiber reconclier 就是类似于协程,在执行算法过程中让出主线程。
双缓存:类似于显卡处理画面,用前缓存区和后缓存区来处理图像,但是这样子出来的图像是一部分慢慢显示出来的,用户的体验感就比较差。
而双缓存区,可以先将计算的中间结果存放在另一个缓存区中,等待全部计算结束,再将缓存区的图形数据一次性复制到显示缓存区,使图像输出更加稳定。
虚拟 DOM 可以看作 DOM 的一个 buffer,会在一次完整的操作之后再把结果应用到 DOM 上,这样就能减少一些不必要的更新,同时还能保证 DOM 的稳定输出。
MVC 模式:
即将视图与数据进行分离
具体流程:图中的控制器是用来监控 DOM 的变化,一旦 DOM 发生变化,控制器便会通知模型,让其更新数据;
模型数据更新好之后,控制器会通知视图,告诉它模型的数据发生了变化;视图接收到更新消息之后,会根据模型所提供的数据来生成新的虚拟 DOM;
新的虚拟 DOM 生成好之后,就需要与之前的虚拟 DOM 进行比较,找出变化的节点;Diff 算法(递归进行比对)
比较出变化的节点之后,React 将变化的虚拟节点应用到 DOM 上,这样就会触发 DOM 节点的更新;
DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。
浏览器的三大进化路线:第一个是应用程序 Web 化,Web 应用移动化,Web 操作系统化
PWA,Progressive Web App 渐进式网页。
渐进式:提供一个渐进式的过度方案,让 Web 应用足部具有本地应用的能力。技术层面不断优化,不断实现本地应用的特性。(比如一些快应用,就类似)
PWA:一套理念,渐进式增强 Web、的优势,并通过技术手短渐进式缩短和本地应用或者小程序的距离
Web 应用相对本地应用的缺点:
缺少离线使用能力。确实消息推送能力。缺失一个入口,需要时可以直接通过桌面打开 web 应用
PWA 采用 Service Worker 解决离线缓存和消息推送,通过引入 manifest.json 解决一级入口的问题。
在页面和网络之间增加一个拦截器,用来缓存和拦截请求

让其运行在主线程之外就是 Service Worker 来自 Web Worker 的一个核心思想,不能将 Service Worker 和但也买呢绑定起来
消息推送也是基于 Service Worker 来实现的
Http 协议是明文传输,存在被窃听、被篡改和被劫持的风险。因此设计之初采用 HTTPS 协议,HTTPS 协议是经过加密的
组件化:对内高聚合,对外低耦合。
阻碍前端组件化的因素,比如在一个页面中嵌入第三方内容,还需要保证第三方的内容样式不会影响到当前内容,或者当前 DOM 不会影响第三方样式。
比如多个 CSS 文件写了标签选择器去定义样式内容,那么很可能被影响。
除了 CSS 的全局属性会阻碍组件化,DOM 也是阻碍组件化的一个因素
使用 WebComponent 的三个步骤:
1.使用 template 属性创建模版
2.创建一个 XXX 的类:在类里面,查找模版内容,创建影子 DOM,再将模版添加到影子 DOM 上
使用 customElements.define 来自定义元素了
3.像 HTML 元素一样使用该元素
1.影子 DOM 中的元素对于整个网页是不可见的
2.影子 DOM 的 CSS 不会影响整个网页的 CSSOM,影子 DOM 内部的 CSS 只对内部元素起作用

超文本传输协议 HTTP/0.9:只有一个请求行,没有返回头消息,返回的文件内容是以 ASCII 字符流传输的。因为传输的文件比较小,且只有 HTML 文件
HTTP/1.0:支持多种类型文件下载,如何去支持,使用请求头和响应头进行协商文件的类型,压缩形式,什么语言,以及什么编码
还加入了状态码,Cache 机制等等。
HTTP/1.0 每次进行 HTTP 通信,都需要建立 TCP 连接,传输数据,断开 TCP 连接三个过程。
而在 HTTP/1.1 中增加了持久化连接的方法,其特点是一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开,那么 TCP 连接会一直保持。
持久化是默认开启,对于同一个域名支持最大 6 个 TCP 持久化连接,可以在请求头上添加:Connection:close
但是这样是有缺陷的,如果某个 TCP 因为某些原因没有返回,那么就会阻塞后面的所有请求,这就是队头阻塞,尝试过使用管线化解决(将多个 HTTP 请求整批提交给服务器的技术)
HTTP1.1 同时提供虚拟主机支持,在 HTTP1.1 的请求头中增加了Host字段,用来表示当前的域名请求。(HTTP1.0 每个域名都绑定一个唯一的 IP,因此一个服务器只能支持一个域名)
HTTP1.1 还对动态生成的内容提供了完美支持,使用Chunk Transfer 机制,服务器将数据分为大小任意的数据块 ,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志
此外 HTTP1.1 引入了客户端 Cookie 和安全机制
对宽带的利用率不高。宽带是指每秒钟最大能发送或者接收的字节数。上行宽带:每秒钟能发送的最大字节数。下行宽带:每秒钟能接收的最大字节数
主要原因:1.TCP 慢启动,他的发送速率是逐步提升的(类似于汽车启动不可能瞬间 120km/h)
2.同时开启多条 TCP 连接,连接之间相互竞争固定的宽带
3.HTTP1.1 队头阻塞的问题,HTTP1.1 使用持久连接时,虽然能公用一个 TCP 管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。
HTTP/2 的多路复用
一个域名只使用一个 TCP 长连接和消除队头阻塞问题

!important 多路复用机制,每份数据都有自己的 ID,在浏览器端,就可以随时将请求发送给服务器,浏览器收到之后筛选相同 ID 的内容,将其拼接为完整的 HTTP 响应数据。将数据一帧帧的数据传输,当收到优先级更高的,服务器可以暂停之前的请求来优先处理关键资源的请求。在同一个连接上并行的传输多个数据,避免 HTTP/1.1 中由于单个请求阻塞导致队头阻塞的问题。
多路复用机制的实现:
添加了一个二进制分帧层,数据经过二进制分帧层之后,转化为一个个带有请求的 ID 编号的帧,通过协议栈将这些帧发送给服务器。
服务器接受到所有帧后,将同一个 ID 的帧合并为一条完整的消息。
同样服务器将响应头、响应行、响应体这些发送到二进制分帧层,分为一个个带有 ID 编号的帧,经过协议栈发送给浏览器,浏览器接受到之后,会根据编号将该帧的数据给不同的请求
HTTP/2 的其他机制
1.可以设置请求的优先级
2.服务器推送(直接将数据提前推送到浏览器)
3.头部压缩(对请求头和响应头进行压缩)
HTTP/2 仍然是基于 TCP 上的,它也会受队头阻塞问题,比如 TCP 连接中任何一个数据包丢失或者延迟,那么该连接上的所有帧都会被暂停运输,直到数据包重传成功。
在网络质量差或者丢包率高的情况下,会影响 HTTP/2 的性能
TCP 存在的问题:
队头阻塞:即由于单个数据丢失造成的阻塞。
建立连接的延时:网络延迟(Round Trip Time),即将一个数据包发送到服务器,然后服务器返回数据包到浏览器的整个往返时间就为 RTT
TCP 建立连接的时候,需要三次握手确认连接成功,即需要 1.5RTT 后才能数据传输。
进行 TLS 连接,TLS 有两个版本 TLS1.2 和 TLS1.3,所花时间大概是 1~2RTT。
TCP 协议的僵化
TCP 协议存在队头阻塞和连接延迟的缺点很难通过优化 TCP 协议进行解决。原因:·中间设备的僵化(互联网各处搭建的各种设备,比如路由器,防火墙,交换机等等)
·操作系统也是导致 TCP 协议僵化的另外一个原因,TCP 协议是通过操作系统内核来实现的,应用程序只能使用不能修改,而且操作系统的更新较为缓慢,因此自由去更新 TCP 非常困难。
HTTP/3 基于 UDP 实现了类似于 TCP 的多路数据流、传输可靠性等功能,这个叫做 QUIC 协议

QUIC 协议的功能:
1.实现了类似 TCP 的流量控制、传输可靠性的功能。虽然 UDP 不提供可靠的传输,但是 QUIC 在 UDP 的基础上增加一层数据保证数据可靠的传递
2.集成了 TLS 加密功能。减少了握手所花费的 RTT 次数
3.实现了 HTTP/2 中的多路复用功能。和 TCP 有所不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流。实现了数据流的单独传输,解决了 TCP 中队头阻塞的问题
4.实现了快速握手。QUIC 基于 UDP,因此 QUIC 可以实现 0RTT 或者 1RTT 建立连接
HTTP/3 的挑战
1.服务器和浏览器端都没有对 HTTP/3 提供比较完整的支持
2.部署 HTTP/3 也存在着非常大的问题
3.中间设备僵化的问题
在没有安全保障的 Web 世界中,我们是没有隐私的,因此需要安全策略来保障我们的隐私和数据的安全。
最基础、最核心的安全策略:同源策略
同源:如果两个 URL 的协议,域名,端口都相同,则两个 URL 同源。
浏览器默认两个相同的源之间,可以互相访问资源和操作 DOM 的。两个不同的源之间若想要相互访问资源或者操作 DOM,那么会有一套基础的安全策略的制约,我们把这称为同源策略。
同源策略主要表现在 DOM、Web 数据和网络这三个层面。
同源策略限制了来自不同源的 JavaScript 脚本对当前 DOM 对象读和写的操作。
对象 opener 就是指向第一个页面的 window 对象,对于两个同源的页面
同源策略限制了不同源的站点读取当前站点的 Cookie、IndexDB、LocalStorage 等数据,这样无法使用第二个页面的
同源策略限制了通过 XMLHttpRequest 等方式将站点的数据发送给不同源的站点。
但是有被 XSS 攻击的风险(即跨站脚本)
XSS 是通过利用网页开发时留下的漏洞,进行攻击,然后攻击者可能得到包括但不限于更高的权限(如执行一些操作)、私密网页内容、会话和 cookie 等各种内容。
为了解决 XSS 攻击,浏览器引入了内容安全策略,称为 CSP。CSP 的核心思想是让服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执行内联 JavaScript 代码
为了解决跨域问题,跨域资源共享(CORS),使用该机制进行跨域访问,从而使跨域数据传输得以安全进行。
不过在实际应用中,经常需要两个不同源的 DOM 之间进行通信,于是浏览器中又引入了跨文档消息机制,可以通过 window.postMessage 的 JavaScript 接口来和不同源的 DOM 进行通信。
同源策略是浏览器的默认行为,它保护了用户的隐私和安全;CSP 是网站开发者的自定义行为,它增强了网页的安全性;CORS 是服务器和浏览器之间的协作行为,它实现了跨域资源共享。
XSS 全称是 cross site scripting,为了与 CSS 攻击区分开才叫 XSS。当页面被注入恶意 JavaScript 时,其可以做到:1.窃取 Cookie 信息,document.cookie,然后通过XMLHttpRequest或者Fetch+CORS功能发送数据模拟用户登陆,进行转账操作。
2.监听用户行为:使用addEventListener监听键入事件,获取信用卡信息,将其发送到恶意服务器
3.修改 DOM,伪造登陆窗口,以此来欺骗用户名和密码等
4.页面生成浮窗广告
恶意脚本的类型:存储型,反射型和基于 DOM 的 XSS 攻击

存储型的特点
恶意 JavaScript 属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript 脚本发送给用户。我们会发现用户将一段含有恶意代码的请求提交给 Web 服务器,Web 服务器接收到请求时,又将恶意代码反射给了浏览器端,这就是反射型 XSS 攻击。
打开http://localhost:3000/?xss=123
http://localhost:3000/?xss=<script>alert('你被xss攻击了')</script>
Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方。
具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。
1.对服务器输入脚本进行过滤或者转码
过滤后变成了
转码后:
2.充分利用 csp,实施严格的 CSP 可以有效地防范 XSS 攻击,具体来讲 CSP 有如下几个功能:
3.使用 HttpOnly 属性
通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的
顾名思义,使用 HttpOnly 标记的 Cookie 只能使用在 HTTP 请求过程中,所以无法通过 JavaScript 来读取这段 Cookie。我们还可以通过 Chrome 开发者工具来查看哪些 Cookie 被标记了 HttpOnly
点击链接中毒,就是 CSRF 的“功劳”
CSRF:Cross-site request forgery,跨站请求伪造。CSRF 攻击就是黑客利用了用户的登录状态,并通过第三方的站点来做一些坏事
自动发起 Get 请求:
当“图片”加载时,自动发起 img 的资源请求,那么服务器认为是一个转账请求的话,就直接执行转账操作了
自动发生 Post 请求
当用户打开该站点之后,这个表单会被自动执行提交;当表单被提交之后,服务器就会执行转账操作。因此使用构建自动提交表单这种方式,就可以自动实现跨站点 POST 数据提交
引诱用户点击链接
这段黑客站点代码,页面上放了一张美女图片,下面放了图片下载地址,而这个下载地址实际上是黑客用来转账的接口,一旦用户点击了这个链接,那么他的极客币就被转到黑客账户上了。
CSRF 攻击必要条件:
与 XSS 攻击不同,CSRF 攻击不会往页面注入恶意脚本
1.充分利用好 Cookie 的 SameSite 属性,Cookie 是维护浏览器和服务器之间登陆状态的一个关键数据。
通常 CSRF 攻击是在 Cookie 上做手脚的
SameSite 有三个值,strict,lax 和 none
Strict 最为严格,会禁止所有第三方的 Cookie
Lax 相对宽松,在跨站点的情况下,从第三方站点的链接打开和从第三方站点提交 Get 方式的表单这两种方式都会携带 Cookie。但如果在第三方站点中使用 Post 方法,或者通过 img、iframe 等标签加载的 URL,这些场景都不会携带 Cookie。
none,而如果使用 None 的话,在任何情况下都会发送 Cookie 数据
2.验证请求的来源站点
在服务器验证请求来源的站点
使用 HTTP 报文中的 Referer 和 Origin。
Referer 是 HTTP 请求头中的一个字段,记录了该 HTTP 请求来源的地址。但是在服务器中验证请求头有点困难,因此又出现了 Origin 属性如果通过 XMLHttpRequest、Fetch 发起跨站请求或者通过 Post 方法发送请求时,都会带上 Origin 属性
3.CSRF Token
使用这个进行验证,浏览器向服务器发送请求时服务器生成一个 CSRF Token。CSRF token 会植入到返回的页面中。如果浏览器要发起 post 等请求就需要带上页面的 CSRF Token,而从第三方站点发起请求无法获取到 CSRF Token 的值
单进程架构的浏览器是不稳定的,甚至可能影响操作系统的安全。
最常见的攻击方式是缓冲区溢出,于 XSS 注入脚本不同。
通过浏览器漏洞攻击,可以入侵到浏览器内部,甚至可以穿透浏览器,在操作系统上做手脚。
浏览器被分为,浏览器内核和渲染内核。浏览器内核被划分为浏览器主进程和网络进程。
渲染进程需要执行 DOM 解析,CSS 解析,网络图片解码等操作,如果渲染进程中存在系统级别的漏洞,那么可能会让恶意站点获取到渲染进程的控制权限,进而又获得操作系统的控制权限,因此需要将渲染进程和浏览器内核相隔开。
将渲染进程和操作系统隔离的就是安全沙箱,浏览器中的安全沙箱是利用操作系统提供的安全技术,让渲染进程在执行过程中无法访问或者修改操作系统中的数据,在渲染进程需要访问系统资源的时候,需要通过浏览器内核来实现,然后将访问的结果通过 IPC 转发给渲染进程。
安全沙箱的最小保护单位是进程。因为单进程浏览器需要频繁访问或修改操作系统的数据,因此单进程浏览器无法被安全沙箱保护
安全沙箱影响各个模块
1.持久存储:现代浏览器将读写文件的操作全部放在了浏览器内核中实现,然后通过 IPC 将操作结果转发给渲染进程。
2.网络访问
渲染进程内部不能直接访问网络,需要通过浏览器内核。不过浏览器内核在处理 URL 请求之前,会检查渲染进程是否有权限请求该 URL,比如检查 XMLHttpRequest 或者 Fetch 是否是跨站点请求,或者检测 HTTPS 的站点中是否包含了 HTTP 的请求。
3.用户交互
渲染进程不能直接访问窗口句柄(允许应用程序与用户交互,允许应用程序在该界面上绘制),因此渲染进程完成以下大的改变
一:渲染进程需要渲染出位图,然后渲染进程将位图发送到浏览器内核,浏览器内核再将位图复制到屏幕上
二:操作系统将用户的输入事件传递给渲染进程,而是将这些事件传递给浏览器内核,由浏览器内核进行判定,如果当前焦点再浏览器地址栏中,则输入事件交给浏览器内核内部处理。如果焦点再当前页面区域,浏览器内核再通过 IPC 将这些事件发送给渲染进程
站点隔离
chrome 将同一站点(包含相同根域名和相同协议的地址)中互相关联的页面放在同一个渲染进程中进行。将标签级的渲染进程重构为 iframe 级的渲染进程。(iframe 嵌入标签,将一个页面嵌入到当前页面的标签)
浏览器安全主要有页面安全,系统安全,网络安全。
页面安全,同源策略,XSS,CSRF
系统安全,安全沙箱
HTTPS 网络安全协议。
HTTP 传输的内容很容易被中间人窃取、伪造和篡改(因为是明文传输的),这种攻击方式也被叫做中间人攻击
HTTP 将数据交给 TCP 层在到达用户电脑之前都有可能被截取数据
那么可以在 HTTP 和 TCP 中间插入一个安全层,对数据进行加密/解密
因此 HTTPS 也不是一个新的协议,通常 HTTP 直接和 TCP 通信,HTTPS 则会先和安全层通信,安全层再和 TCP 进行通信
安全层的主要作用就是对发送的数据加密,对接收的数据解密
对称加密:指加密和解密都是同一套密钥
但是其中传输 client-random 和 service-random 的过程却是明文的,这意味着黑客也可以拿到协商的加密套件和双方的随机数,由于利用随机数合成密钥的算法是公开的,所以黑客拿到随机数之后,也可以合成密钥,这样黑客也会进行解密得到相应的数据
非对称加密算法有 A、B 两把密钥,如果你用 A 密钥来加密,那么只能使用 B 密钥来解密;反过来,如果你要 B 密钥来加密,那么只能用 A 密钥来解密。
在 HTTPS 中,服务器会将其中的一个密钥通过明文的形式发送给浏览器,这个密钥称为公钥;服务器自己留下的称为私钥。公钥每个人都能获取到,而私钥只能服务器持有
浏览器通过公钥进行加密,服务器通过私钥进行解密。虽然这样保证了浏览器向服务器发送数据的安全,但是存在一些问题。
1.非对称加密的效率太低。
2.无法保证服务器发送给浏览器的数据安全。当服务器处理完数据,使用私钥进行加密后发送给浏览器的过程中,黑客可能截取这段数据,并且黑客也能得到公钥,因此能将服务器发送的数据进行篡改,就不能保证安全。
在传输阶段依然使用对称加密,但是对称加密的密钥我们采用非对称加密来传输。
pre-master 是经过公钥加密之后传输的,所以黑客无法获取到 pre-master,这样黑客就无法生成密钥,也就保证了黑客无法破解传输过程中的数据了,实现了数据的加密传输。
但是如果黑客劫持 DNS,将要访问的服务器变为它自己的服务器,那么就有危险了
给服务器颁发证书的权威机构,CA(Certificate Authority),颁发的证书就称为数字证书(Digital Certificate)
申请证书的流程:
这个签名是通过 hash 函数计算得到的信息摘要,CA 后面再使用私钥对信息摘要进行加密,加密后的就是数字签名
注意:申请数字证书是不需要提供私钥的,要确保私钥永远只能由服务器掌握;数字证书最核心的是 CA 使用它的私钥生成的数字签名;内置 CA 对应的证书称为根证书,根证书是最权威的机构,它们自己为自己签名,我们把这称为自签名证书。