预检请求:options
-
预检请求只会在跨域时发送,主要用于服务器基于从预检请求头部获得的信息来判断,是否接受接下来的实际请求。当跨域时请求方式为复杂请求或者(简单请求get,post,head)有自定义的字段或者contentType的类型时,会触发(任1条件不满足就触发)
优化OPTIONS请求:Access-Control-Max-Age 或者 避免触发,如果值为 -1,则表示禁用缓存,每一次请求都需要OPTIONS请求进行检测。
预检请求的条件
浏览器进程
浏览器输入url到页面展示期间,发生了什么?
-
用户在地址栏输入(1如果是搜索内容,合成默认搜索引擎带搜索关键字的url。2如果是url,根据规则加上协议合成完整url),用户输入并键入回车后,标签页的图标进入加载状态,页面还是之前打开的页面内容。在提交文档阶段页面才会被替换
- 浏览器进程通过进程通信(ipc)机制,把url发给网络进程,由网络进程发起求。(网络进程查找本地缓存 =>DNS解析,获取请求域名的服务器ip地址( 没有缓存发请求)=> 利用ip地址建立tcp连接 => 浏览器端构建请求参数,包括请求行,请求头并在设置cookie等,并发送) => 服务器会根据请求信息生成响应数据(包括响应行、响应头和响应体等信息)发给网络进程 => 网络进程接受并解析数据 (1.响应头中的状态码为302, 301为重定向,网络进程会获取响应头的location的url,发新url的请求 2. 浏览器会根据Content-Type的值来决定如何显示响应体的内容,如果Content-Type为text/html为网页,会准备渲染进程,为application/octet-stream为字节流类型,该请求会被提交给浏览器的下载管理器
-
Chrome的默认策略是,每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同域的话,那么新页面会复用父页面的渲染进程,跨域走默认开启新渲染进程
- 网络进程提交文档给渲染进程,文档”是指URL请求的响应体数据。『提交文档』的消息由浏览器进程发出,然后渲染进程和网络进程建立传输数据管道,数据传输完成之后,渲染进程会返回『确认提交』给浏览器进程,然后浏览器进程更新浏览器界面状态,如安全状态、地址栏的URL、前进后退的历史状态,并更新Web页面。
5 开始渲染(构建DOM树、样式计算、布局阶段、分层、绘制、分块、光栅化和合成),并在渲染完毕发消息给浏览器进程,浏览器会停止标签图标上的加载动画。
CSS 的加载和解析并不会阻塞 DOM Tree 的构建,因为 DOM Tree 和 CSSOM Tree 是两棵相互独立的树结构。但是这个过程会阻塞页面渲染,也就是说在没有处理完 CSS 之前,文档是不会在页面上显示出来的,由于js可能会操作之前的Dom节点和css样式,因此浏览器会维持html中css和js的顺序。因此,样式表会在后面的js执行前先加载执行完毕。所以css会阻塞后面js的执行
dom树含有不可见元素如head标签,display:none的元素,布局树中这些将会删除
-
在HTML页面内容被提交给渲染引擎之后,渲染引擎首先将HTML解析为浏览器可以理解的DOM;然后根据CSS样式表,计算出DOM树所有节点的样式(cssom);将dom tree 和 csssom 合并构成布局树(render tree),接着又计算每个元素的几何坐标位置,并将这些信息保存在中rendertree
布局树的构建过程
-
- 为了更加方便地实现复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),图层树的构建之后,渲染引擎的主线程准备好绘制列表后,会把该绘制列表(记录绘制顺序和绘制指令的列表)提交(commit)给合成线程并进行绘制,然后合成线程会将图层划分为图块(tile通常是256x256或者512x512),栅格化线程按照视口附近的图块来优先生成位图(为了优先绘制视口的图块),渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,通常栅格化过程都会使用GPU来加速生成(叫做快速栅格化,GPU栅格化),如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。一旦所有图块都被栅格化,合成线程提交绘制图块的命令『DrawQuad』给浏览器进程,浏览器进程根据该命令,将页面内容绘制到内存中再显示到屏幕上。
图层树
绘制列表
合成线程
图层分块
删格化生成位图
合成和显示
分层:明确定位属性的元素、定义透明属性的元素、使用CSS滤镜的元素等,都拥有层叠上下文属性或者需要剪裁(clip)的地方也会被创建为图层。元素有了层叠上下文的属性或者需要被剪裁,满足这任意一点,就会被提升成为单独一层。
- 为了更加方便地实现复杂的3D变换、页面滚动,或者使用z-indexing做z轴排序效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree),图层树的构建之后,渲染引擎的主线程准备好绘制列表后,会把该绘制列表(记录绘制顺序和绘制指令的列表)提交(commit)给合成线程并进行绘制,然后合成线程会将图层划分为图块(tile通常是256x256或者512x512),栅格化线程按照视口附近的图块来优先生成位图(为了优先绘制视口的图块),渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行的,通常栅格化过程都会使用GPU来加速生成(叫做快速栅格化,GPU栅格化),如果栅格化操作使用了GPU,那么最终生成位图的操作是在GPU中完成的,这就涉及到了跨进程操作。一旦所有图块都被栅格化,合成线程提交绘制图块的命令『DrawQuad』给浏览器进程,浏览器进程根据该命令,将页面内容绘制到内存中再显示到屏幕上。
如何优化重绘和重排
- 将多次改变样式属性的操作合并成一次操作
- 将需要多次重排的元素,position属性设为absolute或fixed,这样此元素就脱离了文档流,它的变化不会影响到其他元素。例如有动画效果的元素就最好设置为绝对定位。
- 由于display属性为none的元素不在渲染树中,对隐藏的元素操作不会引发其他元素的重排。如果要对一个元素进行复杂的操作时,可以先隐藏它,操作完成后再显示。这样只在隐藏和显示时触发2次重排。
js代码编译=>执行(执行上下文和可执行代码)
- 一段JavaScript代码在执行之前需要被JavaScript引擎编译,编译完成之后,才会进入执行阶段(之所以需要实现变量提升,是因为JavaScript代码在执行之前需要先编译。 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为undefined;在代码执行阶段,JavaScript引擎会从变量环境中去查找自定义的变量和函数。)。
-
代码会编译成执行上下文(执行上下文是JavaScript执行一段代码时的运行环境,包括this等,由变量环境和词法环境组成,变量环境是变量提升的部分,词法环境是const, let声明的部分)和可执行代码(声明之外的代码,如赋值,执行等)
js执行流程 -
JavaScript的调用栈
代码
调用栈 -
有let,const的声明的变量放在词法环境中(小型栈,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶,块执行完毕弹出)
变量查找过程 - 词法作用域和作用域链:
词法作用域就是指作用域是由代码中函数声明的位置来决定的,是静态的作用域(变量环境中的outer)也就是词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系。
单个执行上下文中查找变量是从词法环境栈顶部=>词法环境栈顶部底部 => 变量环境的底部 => 变量环境的顶部
多个执行上下文中查找变量是从当前执行上下文中查找 => outer 指向的执行上下文
function bar() {
console.log(myName)
}
function foo() {
var myName = " 极客邦 "
bar()
}
var myName = " 极客时间 "
foo()
// 输出 『极客时间』
// 作用域链由函数声明的时候决定
闭包:
返回的innerBar中有两个方法引用了外部函数foo的test1和myName两个变量,这些变量的集合就称为 foo 函数的闭包,代码编译时,js引擎对内部函数做⼀次快速的词法扫描,当发现其是个闭包时,会在堆空间创建换⼀个“closure(foo)”的对象,并将内部函数引用外部函数的值保存在该对象中,闭包函数的执行上下文会有个引用指向该对象空间地址,foo函数执行完时,foo的执行上下文会出栈。
在外部函数执行完毕时,foo函数的执行上下文从栈顶弹出,但是闭包变量依然保存在内存中,只有闭包函数setName 和 getName才能访问,无论在哪里调用了 setName 和 getName 方法,它们都会背着这个 foo 函数的专属背包
- 闭包回收问题:
如果引用闭包的函数是全局变量,闭包会一直存在直到页面关闭,如果这个闭包以后不再使用就会造成内存泄露。如过引用是个局部变量,等改局部变量的上下文销毁后,下次垃圾回收就会回收这块内存。
一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
function foo() {
var myName = " 极客时间 "
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(" 极客邦 ")
bar.getName()
console.log(bar.getName())
- this:this 是和执行上下文绑定的,也就是说每个执行上下文中都有一个 this。
执行上下文分为:全局执行上下文,函数执行上下文和eval执行上下文,因为this是在执行上下文中的,所以this也有全局执行上下文中的 this、函数中的 this 和 eval 中的 this。
如何设置执行上下文的this指向其他对象,1. 通过函数的 call 方法设置,2. 通过对象调用方法设置, 3.通过构造函数中设置
// 1通过函数的 call 方法设置
let bar = {
myName : " 极客邦 ",
test1 : 1
}
function foo(){
this.myName = " 极客时间 "
}
foo.call(bar) // 修改了bar对象
console.log(bar) // {myName: ' 极客时间 ', test1: 1}
console.log(myName) // 报错,myName未定义
// 2通过对象调用方法设置
function t(){
console.log(this)
var myObj = {
name : " 极客时间 ",
showThis: t
}
}
// 可以认为 JavaScript 引擎在执行myObject.showThis()时,将其转化为myObj.showThis.call(myObj)
myObj.showThis() // {name: ' 极客时间 ', showThis: ƒ},对象调用,this指向该对象
//3 通过构造函数中设置
function CreateObj(){
this.name = " 极客时间 "
}
/* new的作用:
var tempObj = {}
CreateObj.call(tempObj)
return tempObj
*/
var myObj = new CreateObj() // {name: ' 极客时间 '}
总结:
- 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window(只要直接调用函数,该函数内this指向window)。
- 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
- => 箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数。
function test() {
this.name = 12
console.log(this, this.name) // 打印 obj,和12 this指向obj
var myObj = {
name : " 极客时间 ",
showThis: function() {
this.name = " 极客邦 "
console.log(this)
}
}
var show = myObj.showThis //跟show = function一样,因为函数是存在堆中,只是修改了指引。
show(); // 直接执行,this指向window
var foo = function(){
this.name = " 极客邦 "
console.log(this)
}
foo(); // 直接执行,this指向window
}
如何解决:嵌套函数中的 this 不会从外层函数中继承
~第一种是把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数
~第二种是继续使用 this,但是要把嵌套函数改为箭头函数
如何解决:普通函数中的 this 默认指向全局对象 window
~使用函数时,使用call方法显示的调用。
~设置严格模式”来解决,严格模式下,函数的执行上下文中的 this 值是 undefined。
js内存空间
JavaScript 是一种弱类型(支持隐式类型转换)的、动态(不声明数据类型,运行过程中检查数据类型)的语言。
- 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。
- 动态,意味着你可以使用同一个变量保存不同类型的数据
为什么不把所有数据都放到栈中?
答:栈空间⼤了 话,所有的数据都存放在栈空间⾥⾯,那么会影响到上下⽂切换的效率,进⽽⼜影响到整个程序的执⾏效率。
function foo(){
var a = " 极客时间 "
var b = a
var c = {name:" 极客时间 "}
var d = c
}
foo()
v8 垃圾回收
esp是记录当前执⾏状态的指针,当⼀个函数执⾏结束之后,JavaScript引擎会通过向下移动ESP来销毁该函数保存在栈中的执⾏上下⽂,(此时执⾏上下⽂就处于⽆效状态了,不过保存在堆中的两个对象依然占⽤着空间)回收堆中的垃圾数据,就需要⽤到JavaScript中的垃圾回收器。
V8 中会把堆分为新生代和老生代两个区域,新生代(1-8m的容量)中存放的是生存时间短的对象,老生代(容量大)中存放的生存时间久的对象(代际假说:1大部分对象在内存中存在的时间很短,2不死的对象,会活得更久)。
垃圾回收器的原理:(1标记还在使用和不再使用的对象,2在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象,3做内存整理,整理内存碎片(内存中不连续空间))
- 副垃圾回收器,主要负责新生代的垃圾回收。
- 主垃圾回收器,主要负责老生代的垃圾回收。
-
新生代垃圾回收器(副垃圾回收器)
新生区的对象有两个特点「小对象」和「存活时间短」。使用Scavenge算法,每次需要将存货对象复制到空闲区域,复制操作成本大耗时所以新生空间会被设置的较小。因为很容易被存货的对象装满整个区域,引擎采用了「对象晋升策略」=> 经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
Scavenge 算法,把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。新加入的对象都放入对象区域,当对象区域快写满时,执行一次垃圾回收(首先对对象区的垃圾做标记,标记完成后回收器把存活的对象复制到空闲区域,同时把这些对象有序排列,相当于完成内存整理。完成复制后将对象区域与空闲区域进行角色翻转)
- 老生代垃圾回收器(主垃圾回收器)
老生区对象有两个特点「大对象」和「存货时间长」。主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收。
- 标记阶段:递归遍历一组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
-
垃圾清除:将标记为垃圾数据的清除,对⼀块内存多次执⾏标记-清除算法后,会产⽣⼤量不连续的内存碎⽚,碎⽚过多会导致⼤对象⽆法分配到⾜够的连续内存,又使用标记-整理(Mark-Compact)算法,让所有存活的对象都向⼀端移动,然后直接清理掉端边界以外的内存。
image.png
-
全停顿(Stop-The-World)
因为js运行在主线程上,执行垃圾回收算法会将正在执行的js脚本暂停。新生区的垃圾回收耗时短影响不大,老生区耗时较大。为了避免老生区一次垃圾回收所占用线程时间太长,V8使用增量标记(Incremental Marking)算法,将标记过程分为一个个的子标记过程,并让让垃圾回收标记和 JavaScript 应用逻辑交替进行,防止用户因为垃圾回收任务而感受到页面的卡顿。
编译器和解释器
-
字节码比机器码所占⽤的空间少很多,减少西东的内存使用。
-ast的生成主要两个步骤:1分词(tokenize),⼜称为词法分析,将源码拆解成token => 2解析(parse),⼜称为语法分析(如果语法错误,抛出『语法错误』),将上⼀步⽣成的token数据,根据语法规则转为 AST。
ast例子:https://resources.jointjs.com/demos/javascript-ast
-
js是解释型语言,js代码 => ast + 执行上下文 => 解释器(Ignition)根据ast生成字节码,并一行行解释执行字节码 => 如果执行字节码时,发现有热点代码(HotSpot),编译器 TurboFan把热点的字节码优化编译为机器码,再次执行该段代码,直接执行编译后的机器码。 => 不是热点代码,解释成机器码。
字节码配合解释器和编译器技称为即时编译(JIT)
js事件循环
在主线程上执行的任务包括:
1渲染事件(如解析DOM、计算布局、绘制)
2⽤⼾交互事件(如⿏标点击、滚动⻚⾯、放⼤缩⼩等)
3JavaScript脚本执⾏事件
4⽹络请求完成、⽂件读写完成事件。-
把消息队列中的任务称为宏任务 ,每个宏任务中都包含了⼀个微任务队列,在执⾏宏任务的过程中,如果遇到微任务(promise.then(),MutationObserver的callback等)则添加到微任务列表中,等宏任务中的主要功能都完成之后,渲染引擎并不着急去执⾏下⼀个宏任务,⽽是执⾏当前宏任务中的微任务(执⾏微任务过程中产⽣的新的微任务 并不会推迟到下个宏任务中执⾏,⽽是在当前的宏任务中继续执⾏),直到执行完微任务才开始执行下一个宏任务。
-
微任务队列执行时机(检查点):在主函数执⾏结束之后、当前宏任务结束之前。所谓主函数执⾏结束,就JavaScript引擎准备退出全局执⾏上下⽂ 并清空调⽤栈的时候,JavaScript引擎会检查全局执⾏上下⽂中的微任务队列。
promise 实现用于理解(Promise 采用了回调函数延迟绑定技术)
1:需要将回调函数onResolve的返回值穿透到最外层
2:回调函数延迟绑定
3:捕获异常:Promise对象的错误具有“冒泡”性质,会⼀直向后传递,直到被onReject函数处理或catch语句捕获为⽌。
Promise 之所以要使用微任务是由 Promise 回调函数延迟绑定技术导致的(promise是微任务的原因)
function Bromise(executor) {
var onResolve_ = null
var onReject_ = null
// 模拟实现 resolve 和 then,暂不支持 rejcet
this.then = function (onResolve, onReject) {
onResolve_ = onResolve
};
function resolve(value) {
// 此处实现可以使用微任务
//setTimeout(()=>{
onResolve_(value)
// },0)
}
executor(resolve, null);
}
function executor(resolve, reject) {
resolve(100)
}
// 将 Promise 改成我们自己的 Bromsie
// new 时,执行Bromise的resolve函数,如果调用时executor的resolve是同步代码,则会报错Uncaught
// "TypeError: onResolve_ is not a function",因为还没有执行到demo.then去注册回调函数
// 所以在Bromise中延迟执行onResolve_(value),异步执行时,demo.then已经执行注册了。
let demo = new Bromise(executor)
function onResolve(value){
console.log(value)
}
demo.then(onResolve)
页面渲染过程
- 当渲染进程接收HTML⽂件字节流时,会先开启⼀个预解析线程,HTML预解析器识别出来了有CSS⽂件和 JavaScript⽂件需要下载,然后就同时发起这两个⽂件的下载请求,提前下载这些数据。(应该是preload)
-
js执行时,必须依赖cssom,渲染引擎不知道js是否操作了样式表,在js执行的位置获取dom树未构建完的元素,为undefined。
分层和合成机制
显卡前缓冲区用于显示,后缓冲区用于写入。
- 显示器显示图像:每秒固定读取60(60hz)次显卡中的「前缓冲区」中的图像,并将读取的图像显⽰到显⽰器上。
- 显卡:合成新的图像,将图像保存到「后缓冲区」,⼀旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,通常情况下,显卡的更 新频率和显⽰器的刷新频率是⼀致的。
后缓冲区的图片生成:重排 => 重绘 => 合成。(其中重排和重绘操作都是在渲染进程的主线程上执⾏的,⽐较耗时;⽽合成操作是在渲染进程的合成线程上执⾏的,执⾏速度快,且不占⽤主线程)
合成(分层、分块和合成): 布局树 => 层树(Layer Tree,树中每个节点对应一个图层 )=> 绘制列表(将绘制指令组合成⼀个列表,如图层要设置的背景为⿊⾊,并在中心画圆,指令:|Paint BackGroundColor:Black | Paint Circle|)=> 光栅化(光栅化就是按照绘制列表中的指令⽣成图⽚,每⼀个图层都 对应⼀张图⽚,合成线程将这些图片合成"一张"图片到后缓冲区)
1 分层是从宏观上提升了渲染效率,那么分块则是从微观层⾯提升了渲染效率,分块涉及「纹理上传」,在⾸次合成图块的时候使⽤⼀个低分辨率的图⽚,先显示底分辨率,合成器继续绘制正常⽐例的⽹⻚内容再替换。
2 需要重点关注的是,合成操作是在合成线程上完成的,这也就意味着在执⾏合成操作时,是不会影响到主线程执⾏的。这就是为什么经常主线程卡住了,但是CSS动画依然能执⾏的原因。
3 可以使⽤合成线程来处理CSS特效或者动画的情况,就尽量使⽤will-change来提前告诉渲染引擎,让它为该元素准备独⽴的层,但会增加内存。
.box { will-change: transform, opacity; }
http
- http/0.9:只是用于传输html。
- http/1.0:增加了请求行,请求头,响应头,响应体等,支持了【文件类型,编码,语言和压缩】【状态码】【Cache机制 】和【⽤⼾代理ua】等,1.0的问题:HTTP/1.0每进⾏⼀次HTTP通信,都需要经历建⽴TCP连接、传输HTTP数据和断开TCP连接三个阶段。
- http/1.1:
1 改进持久连接(默认开启):HTTP/1.1中增加了持久连接的⽅法,它的特点是在⼀个TCP连接上可以传输多个HTTP H 请求,只要浏览器或者服务器没有明确断开连接,那么该TCP连接会⼀直保持
2 不成熟的HTTP管线化:【队头阻塞问题:持久连接虽然能减少TCP的建⽴和断开次数,但是它需要等待前⾯的请求返回之后,才能进⾏下⼀次请求。 如果TCP通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后⾯的所有请求】HTTP/1.1中的管线化是指将多个HTTP请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。(被放弃)
3 提供虚拟主机的⽀持:1.0时,因为1个域名对应1个唯一ip,所以一台服务器只能支持一个域名,但是随着虚拟主 机技术的发展,需要实现在⼀台物理主机上绑定多个虚拟主机,每个虚拟主机都有⾃⼰的单独的域名,这些 单独的域名都公⽤同⼀个IP地址。HTTP/1.1的请求头中增加了Host字段,⽤来表⽰当前的域名地址,服务器就可以根据不同的Host值做不同的处理。
4 对动态⽣成的内容提供了完美⽀持:
HTTP/1.0时,需要在响应头中设置完整的数据⼤⼩,如Content-Length: 901,但动态内容无法设置数据大小,导致浏览器不知道何时接受完所有数据。HTTP/1.1通过引⼊Chunk transfer机制来解决这个问题,服务器将数据分割许多任意大小的数据块(块中有上个数据块的长度),最后发送零长度块作为数据发送完成标志。
5 客⼾端Cookie、安全机制 。
http1.1的问题: 1. TCP的慢启动(tcp协议用逐渐加快的速度发送数据,速度最终达到峰值),是TCP为了减少⽹络拥塞的⼀种策略。2. 同时开启了多条TCP连接,那么这些连接会竞争固定的带宽。3. HTTP/1.1队头阻塞的问题(使⽤持久连接时,虽然能公⽤⼀个TCP管道,但是在⼀个管道中同⼀时刻只能处理⼀个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态)。
- http/2.0:
主要实现了:多路复⽤
⼀个域名只使⽤⼀个TCP⻓连接来传输数据(只有1次慢启动,解决多个TCP连接竞争带宽),将请求分成⼀帧⼀帧的数据去传输(服务器可以处理优先级高的请求,反正不用按顺序返回,比如js,css资源),每个请求都有1个id,浏览器接收到之后,会筛选出相同ID的内容,将其拼接为完整的HTTP响应数据(解决队头阻塞问题)。
虽然HTTP/2解决了HTTP/1.1中的队头阻塞问题,但是HTTP/2依然是基于TCP协议的,⽽TCP协议依然存在 数据包级别的队头阻塞问题(TCP传输过程中也是把⼀份数据分为多个数据包的。当其中⼀个数据包没有按照顺序返回,接收端会⼀直保持连接等待数据包返回,这时候就会阻塞后续请求),随着丢包率的增加,HTTP/2的传输效率也会越来越差,丢包率到2%时,1.1比2更好。
新增了⼀个⼆进制分帧层。这些数据经过⼆进制分帧层处理之后,会被转换为⼀个个带有请求ID编号的帧,通过协议栈将这些帧发送给服务器。
HTTP/2其他特性:
1.可以设置请求的优先级,2.服务器推送(可以将html文件中要使用的css和js文件一并发给浏览器)3.头部压缩(对请求头和响应头进行压缩)
浏览器的同源策略
同源策略主要表现在DOM、Web数据和⽹络这三个层⾯。
- DOM层⾯ ,同源策略限制了来⾃不同源的JavaScript脚本对当前DOM对象读和写的操作。
- 数据层⾯ ,同源策略限制了不同源的站点读取当前站点的Cookie、IndexDB、LocalStorage等数据
- ⽹络层⾯。同源策略限制了通过XMLHttpRequest等⽅式将站点的数据发送给不同源的站点。
浏览器出让了同源策略的哪些安全性?
- ⻚⾯中可以嵌⼊第三⽅资源,如css,js,图片等资源,会涉及XSS(Cross Site Scripting跨站脚本攻击,区分css故意改XSS)的安全问题,所以引入csp限制(服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执⾏内联JavaScript代码)。
- 跨域资源共享(cors)和跨⽂档消息机制(不同源的页面,无法相互操作DOM,可以通过window.postMessage进⾏通信)。
XSS跨站脚本攻击
主要有存储型XSS攻击、反射型XSS攻击 存 和基于DOM的XSS攻击 基 三种⽅式来注⼊恶意脚本。
-
存储型XSS攻击
在网站中的输入,嵌入有恶意脚本的<script src=http:/>,保存后网站未做过滤转义等直接存储了,在下一次请求时把带恶意脚本的html发送给用户。
-
反射型XSS攻击 :⽤⼾将⼀段含有恶意代码的请求提交给Web服务器,Web服务器接收到请求时, ⼜将恶意代码反射给了浏览器端。
网站直接将用户的输入参数,展示在页面上,服务器不会存储恶意脚本。(在现实⽣活中,⿊客经常会通过QQ群或者邮件 等渠道诱导⽤⼾去点击这些恶意链接)
- 基于DOM的XSS攻击
基于DOM的XSS攻击是不牵涉到⻚⾯Web服务器的,共同点是在Web资源传输过程或者在⽤⼾使⽤⻚⾯的过程中 修改Web⻚⾯的数据。
- 如何阻止XSS攻击(存储型XSS攻击和反射型攻击是服务端的安全漏洞,基于dom的xss攻击是前端的安全漏斗)
- 服务器对输⼊脚本进⾏过滤或转码 2.充分利⽤CSP 设置如<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'n one';">(功能可以:限制加载其他域下的资源⽂件,禁⽌向第三⽅域提交数据,禁⽌执⾏内联脚本和未授权的脚本,提供了上报机制,发现xss攻击) 3.使⽤HttpOnly属性(很多XSS攻击都是来盗⽤Cookie的,设置后document.cookie获取不到)4 还可以通过添加验证码防⽌脚本冒充⽤⼾提交危险操作等
CSRF(Cross-site request forgery)攻击,又叫跨站请求伪造
一个典型的CSRF攻击有着如下的流程:
- 受害者登录a.com,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了b.com。
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com以受害者的名义执行了act=xx。
攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
几种常见的攻击类型: GET类型(img等标签),POST类型(隐藏表单提交)和链接类型(a链接)
-
如何防护:
a. 阻止不明外域的冒充访问:
1.同源检测:可以通过请求头中的Origin Header,Referer Header判断是否来自外域。
2.Cookie 的 SameSite 属性,设置为Strict时:浏览器会完全禁⽌第三⽅ Cookie。Lax时:从第三⽅站点的链接打开和从第三⽅站点提交Get⽅式的表单这 两种⽅式都会携带Cookie,其他post方法,img,iframe标签等加载不会携带。None时:在任何情况下都会发送Cookie数据。
b.提交时要求信息(本域才能获取到)验证
1.CSRF Token:
在浏览器向服务器发起请求时,服务器⽣成⼀个CSRF Token,每次页面加载时遍历整个DOM树,对于DOM中所有的a和form标签后加入Token,用户提交请求给服务器时,服务器需要判断Token的有效性。
<form action="https://time.geekbang.org/sendcoin" method="POST">
<input type="hidden" name="csrf-token" value="nc98P987bcpncYhoadjoiydc9ajDlcn">
<input type="text" name="user">
<input type="text" name="number">
<input type="submit">
</form>
2.双重Cookie验证
利用CSRF攻击不能获取到用户Cookie的特点,我们可以要求Ajax和表单请求携带一个Cookie中的值。
以下流程:
- 在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串(例如csrfcookie=v8g9e4ksfhw)。
- 在前端向后端发起请求时,取出Cookie,并添加到URL的参数中(接上例POST https://www.a.com/comment?csrfcookie=v8g9e4ksfhw)。
- 后端接口验证Cookie中的字段与URL参数中的字段是否一致,不一致则拒绝。
此方法相对于CSRF Token就简单了许多。可以直接通过前后端拦截的的方法自动化实现。但是大型网站上的安全性还是没有CSRF Token高,因为前端域名www.a.com和后端接口api.a.com不同域,cookie必须种在a.com下,子域随意访问,但是子域被xss攻击可以获取或者修改cookie,再对www.a.com进行CSRF攻击。
https
- 客户端发送"client hello"消息:消息包含了客户端所支持的 TLS 版本和密码组合以供服务器进行选择,还有一个"client random"随机字符串。
- 服务器响应"server hello"消息:消息包含了数字证书,服务器选择的密码组合和"server random"随机字符串。
- 客户端对服务器发来的证书进行验证,确保对方的合法身份(检查数字签名,证书链,有效期等),如果验证通过则进行下一步。
- 客户端利⽤client-random和service-random计算出来pre-master,然后利⽤公钥对pre- master加密并发送。
- 服务器用私钥解密出pre-master数据,并返回确认消息(服务器和浏览器就有了共同的client-random、service-random和pre-master)。
- 客户端和服务端使用这三组随机数通过相同的算法⽣成对称密钥master secret(对称密钥只有两方知道,因为pre-master是非对称加密无法破解,且CA证书保证了传输过程中被劫持会被识破),后续数据传输都使用该对称密钥加密(因为对称加密比非对称加密效率高)
浏览器缓存
- 按缓存位置分类 (memory cache, disk cache, Service Worker 等)
a. 从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置(设置no-store时,memory cache 也不存储)memory cache主要有两块,
- preloader 请求够来的资源就会被放入 memory cache 中,供之后的解析执行操作使用。(浏览器解析html的时候,预下载js,css)
- preload如:<link rel="preload">。这些显式指定的预加载资源
b. disk cache:是持久存储的,允许相同的资源在跨会话,甚至跨站点的情况下使用。 disk cache 会严格根据 HTTP 头信息中的各类字段来判定是否缓存,是否过期,是否重新请求等
c. ServiceWorker cache:Cache和CacheStorage都是Service Worker API下的接口, 一般会用Service Worker和cacheStorage做离线开发。Service Worker 没能命中缓存,会使用 fetch()继续获取资源,继续memory cache 或者 disk cache 进行下一次找缓存的工作(经过 Service Worker 的 fetch() 方法获取的资源,即便它并没有命中 Service Worker 缓存,甚至实际走了网络请求,也会标注为 from ServiceWorker)
// serviceWorker.js
self.addEventListener('install', e => {
// 当确定要访问某些资源时,提前请求并添加到缓存中。
// 这个模式叫做“预缓存”
e.waitUntil(
caches.open('service-worker-test-precache').then(cache => {
return cache.addAll(['/static/index.js', '/static/index.css', '/static/mashroom.jpg'])
})
)
})
self.addEventListener('fetch', e => {
// 缓存中能找到就返回,找不到就网络请求,之后再写入缓存并返回。
// 这个称为 CacheFirst 的缓存策略。
return e.respondWith(
caches.open('service-worker-test-precache').then(cache => {
return cache.match(e.request).then(matchedResponse => {
return matchedResponse || fetch(e.request).then(fetchedResponse => {
cache.put(e.request, fetchedResponse.clone())
return fetchedResponse
})
})
})
)
})
- 按失效策略分类 (强缓存和协商缓存都属于disk cache ( HTTP cache))
memory cache 是浏览器为了加快读取缓存速度而进行的自身的优化行为,不受开发者控制,也不受 HTTP 协议头的约束,算是一个黑盒。Service Worker 是由开发者编写的额外的脚本,且缓存位置独立,出现也较晚。
a. 强缓存:字段是 Expires和Cache-control,(f5刷新时会让强缓存过期,走协商缓存)
- Expires:是HTTP 1.0 的字段,是一个绝对的时间 (当前时间+缓存时间)如
Expires: Thu, 10 Nov 2017 08:45:11 GMT
缺点:
1.由于是绝对时间,用户可能会将客户端本地的时间进行修改,而导致浏览器判断缓存失效,重新请求该资源。此外,即使不考虑用户修改,时差或者误差等因素也可能造成客户端与服务端的时间不一致,致使缓存失效。
2.写法太复杂了。表示时间的字符串多个空格,少个字母,都会导致非法属性从而设置失效。
- Cache-control: HTTP/1.1新增,字段表示资源缓存的最大有效时间,是相对时间。
no-cache从语义上表示下次请求不要直接使用缓存而需要比对,重点是下次,如同一个页面中使用好几次,则是直接使用。
no-store则是不缓存,用一次请求一次。
Cache-control: max-age=2592000
优点:
即使客户端时间发生改变,相对时间也不会随之改变,这样可以保持服务器和客户端的时间一致性。而且 Cache-control 的可配置性比较强大。
b. 协商缓存:字段是Last-Modified & If-Modified-Since,Etag & If-None-Match(状态码304继续使用否则是新数据)
- Last-Modified & If-Modified-Since
- 在下次发请求确认该缓存是否改变时,将上次响应数据的Last-Modified 的值写入到请求头的 If-Modified-Since 字段
- 服务器会将 If-Modified-Since 的值与 Last-Modified 字段进行对比。如果相等,则表示未修改,响应 304;反之,则表示修改了,响应 200 状态码,并返回数据。
Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
缺点:
1.如果资源更新的速度是秒以下单位,那么该缓存是不能被使用的,因为它的时间单位最低是秒。
2.如果文件是通过服务器动态生成的,那么该方法的更新时间永远是生成的时间,尽管文件可能没有变化,所以起不到缓存的作用。
- Etag & If-None-Match
etag 会基于资源的内容编码生成一串唯一的标识字符串
etag: "FllOiaIvA1f-ftHGziLgMIMVkVw_"
浏览器行为:
实际缓存的应用模式:
1.不常变化的资源:给它们的 Cache-Control 配置一个很大的 max-age=31536000 (一年),为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,达到更改引用 URL 的目的。
2.经常变化的资源:Cache-Control: no-cache,特点URL 不能变化,但内容可以(且经常)变化
3.强缓存和协商缓存结合反例问题:强缓存时间较短时,如果出现老index.js缓存被清理,老index.html请求新index.js,不同版本的资源组合导致报错。
sessionStorage和localStorage
总结:他们都是对象,数据都绑定到同源下,存储大小限制为 5MB+,具体取决于浏览器,设置的value为对象时,需要转成字符串。
localStorage:不同tab的同源页面能共享。持久化存储,没有过期时间,关闭浏览器数据也还在
sessionStorage:只有同一个标签页的多个同源iframe能共享。是当前会话存储对象,页面关闭数据删除。