框架
Vue
MVVM是什么?
Model-View-ViewModel
, Model
表示数据模型层。view
表示视图层, ViewModel
是 View
和 Model
层的桥梁,数据绑定到 viewModel
层并自动渲染到页面中,视图变化通知 viewModel
层更新数据。
Vue 的生命周期
什么时候被调用?
- beforeCreate :实例初始化之后,数据观测之前调用
- created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、
watch/event
事件回调。无$el
. - beforeMount:在挂载之前调用,相关
render
函数首次被调用 - mounted:了被新创建的
vm.$el
替换,并挂载到实例上去之后调用改钩子。 - beforeUpdate:数据更新前调用,发生在虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
- updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用改钩子。
- beforeDestroy:实例销毁前调用,实例仍然可用。
- destroyed:实例销毁之后调用,调用后,Vue 实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
父子组件更新顺序
父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
子组件更新过程:
影响到父组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updated
不影响父组件: 子beforeUpdate -> 子updated
Vue跨组件通信
- props
- 父子组件使用
$parent、$children
- 使用 $ref ,指定某一组件
- vuex
- inject,可以向更深层级组件注入数据
- Bus事件总线,利用一个Vue实例的$on接口
vue 是如何实现响应式数据的呢?(响应式数据原理)
Vue2:Object.definProperty
重新定义 data
中所有的属性, Object.definProperty
可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。具体的过程:首先 Vue 使用 initData
初始化用户传入的参数,然后使用 new Observer
对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value)
对对象进行处理,内部使用 defineReactive
循环对象属性定义响应式变化,核心就是使用 Object.definProperty
重新定义数据。
vue 中是如何检测数组变化
将数组每一项经过
Oberserver
类进行响应式定义;将经过了响应式变化数组的原型替换为另一个原型,这个原型上重写了可以改变原数组的方法,目的是监听数组变更(函数劫持)
双向绑定以及相关原理
双向绑定到底要答什么?我一直心里都有疑问,网上的答案参差不齐:有的只答到响应式部分,有的答案只答了通知更新的过程,有的答案洋洋洒洒近千字到把vue的整套过程都答出来了。
双向绑定是什么?双向绑定是指 视图 和 数据 同步更新。具体而言,就是 v-model
, v-model
是 vue 的语法糖,实质是 value 的单向绑定和 oninput/onchange 事件侦听的语法糖。
所以实际上,我们还是需要把 单向绑定 讲清楚了,加上对事件的侦听的点就可了。
单向绑定
主要是: 数据劫持 和 模板编译
在 Vue 文件导出时,模板被编译成 render 函数,render 函数的返回值是对应模板的 virtual dom;
在 create 阶段,Vue 将数据转为响应式数据;响应式数据上每个属性(包括深层的属性)上对应一个 Dep实例,同时被设置了get,set函数劫持数据;
在某个 watcher 首次访问属性时,调用 get 函数,这个 watcher 会被收集到 Dep 实例内部 watcher 数组中;在数据更改时,调用 set 函数,对应 Dep 实例通知 watcher 数组中所有 watcher
在 moute 阶段,Vue new 了一个 Watcher,监听vm实例且传入的函数内部调用了 render 的函数;调用 render 生成 vdom 的过程中,watcher 首次访问模板上绑定的数据,被添入各个数据对应的 Dep 的 watcher 数组中
数据改变,调用 set 函数,对应的 Dep 实例通知自己 watcher 数组中所有的 watcher 更新其对应的视图
单向绑定讲到这里应该够了,流程后面就是更新视图具体过程
- nextTick 收集watcher
- watcher 调用 update 生成新的 vdom
- diff 算法对比新旧 vdom
- patch 过程中更新真实dom
再加上 v-model 原理
父组件 v-model
监听 oninput/onchange
,
子组件触发oninput/onchange
事件,同时将值传出,
父组件触发回调,将传入值赋给绑定的数据
就实现了 视图与数据同步更新 的双向绑定。
一个较为全面答案
- 响应式:对需要观察的数据对象进行递归遍历,包含子属性对象的属性,设置set和get特性方法;当给这个对象的某个值赋值时,会触发绑定的set特性方法,就能起到监听数据的变化。
- 模板编译绑定数据:用compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,就会收到通知,并更新视图。
- 通知更新过程:Watcher订阅者是Observer和Compile之间通信的桥梁:在自身实例化时向属性订阅器dep里面添加自己;自身必须有一个update()方法;在dep.notice()发布通知时,能调用自身的update()方法,并触发Compile中绑定的回调函数。
- MVVM及双向绑定的体现:MVVM是数据绑定的入口,整合了Observer,Compile和Wathcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher来搭起Observer和Compile之间的通信桥梁,达到数据变化通知视图更新的效果,利用视图交互,变化更新model数据的双向绑定效果。
Virtual Dom 是什么?有什么优势?
Virtual Dom是摸拟 dom 的 js对象,它与dom有着一一对应的映射关系。
虚拟 DOM 不会立马进行排版与重绘操作
虚拟 DOM 进行频繁修改,然后一次性比较并修改真实 DOM 中需要改的部分,最后在真实 DOM 中进行排版与重绘,减少过多DOM节点排版与重绘损耗
虚拟 DOM 有效降低大面积真实 DOM 的重绘与排版,因为最终与真实 DOM 比较差异,可以只渲染局部。
Virtual DOM提供了跨平台能力,比如 uniapp、taro、react native等,Virtaul DOM 内部可以有对不同平台的组件的处理方式,如 Vue 就提供自定义适配器的接口
nextTick什么原理?
Vue源码详解之nextTick:MutationObserver只是浮云,microtask才是核心!
在宏任务一轮中,watcher在收到变更通知后会把自己放入一个Watcher数组中 ,其他的外部nextTick回调也会被收集到nextTick回调队列中。
一轮宏任务结束后,Vue将执行nextTick回调队列的函数推入微任务中(使用promise.then或者Mutation Observer),nextTick回调队列中,首个回调是执行遍历watcher数组使其更新对应dom的操作函数,之后再执行外部收集的nextTick回调,所以它保证了外部收集的nextTick回调一定是在dom更新之后执行。
diff算法原理
updateChildren
需要有图示才好理解,请看 详解vue的diff算法,建议把后面例子自己手写一遍。
patch
输入一个组件的新旧根节点(patch),比较是否相同(sameNode)。
如果新旧节点相同,才会继续往下比较他们的子节点(patchNode);若不同,直接创造新的DOM节点替换旧的DOM。
patchVnode
新旧仅有文本,替换文本即可
如果
oldVnode
有子节点而Vnode
没有,则删除el
的子节点如果
oldVnode
没有子节点而Vnode
有,则将Vnode
的子节点真实化之后添加到el
文本替换,旧有新无删除,旧无新有添入
如果两者都有子节点,则执行
updateChildren
函数比较子节点
updateChildren(主要算法)
这个函数是diff算法中最核心的函数,目的是尽量复用已有的节点
- 将
Vnode
的子节点Vch
和oldVnode
的子节点oldCh
提取出来 oldCh
和vCh
各有两个头尾的变量StartIdx
和EndIdx
,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key
,就会用key
进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx
表明oldCh
和vCh
至少有一个已经遍历完了,就会结束比较。
oldS、oldE、S、E
两两做sameVnode
比较,有四种比较方式,当其中两个能匹配上那么真实dom中的相应节点会移到Vnode相应的位置
如果是oldS和E匹配上了,那么oldS的真实dom中的会被移到最后
如果是oldE和S匹配上了,那么oldE的真实dom中的最后一个节点会移到最前
同时两边的游标也会向内进
如果四种匹配没有一对是成功的,那么遍历
oldChild
,S
挨个和他们匹配,匹配成功就在真实dom中将成功的节点移到最前面,如果依旧没有成功的,那么将S对应的节点
插入到dom中对应的oldS
位置,oldS
和S
指针向中间移动。
Vuex
使用actions的场景
Mutation 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
Action Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。 .
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
- actions并非中并非一定要有异步commit
- 异步提交commit也并非一定需要放在actions中,可以在组件中异步逻辑后用commit提交
actions主要的作用是 封装逻辑 。
比如,一个改变登录状态的逻辑,它被不同组件使用,此时将 登录状态的请求与变更状态一同放入 actions 就是不二之选
const actions = {
async login({ commit }, userInfo) {
const loginResult = await login({
userName: userInfo.username,
pwd: userInfo.password,
verifyCode: userInfo.verifyCode
})
if (loginResult.code === 1) {
commit('SET_LOGIN_STATUS', true)
}
return loginResult
}
}
Vue Router
hash模式与history模式的区别
- hash模式通过改变哈希符
#
后的路径字符串,结合浏览器提供onhashchange
监听路由改变;同时,hash模式在刷新后,请求的还是根路径的页面,返回后前端再通过路径字符串进行重定向 - history模式去掉了
#
,利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法,当它用户在页面前进或者后退时,浏览器并不会立即向后端发送请求;但是在刷新页面时,浏览器就会按照当前的url请求服务器,如果服务端没有进行配置就会导致404。解决方法是服务端在无法匹配路径时默认重定向到 index.html
$router
和 $route
分别是什么?
router 为 VueRouter 的实例,是一个全局路由对象,包含了路由跳转的方法、钩子函数等。 route 是路由信息对象||跳转的路由对象,每一个路由都会有一个route对象, 是一个局部对象,包含path,params,hash,query,fullPath,matched,name
等路由信息参数
如何传参?
Params
只能使用name,不能使用path,参数不会显示在路径上,浏览器强制刷新参数会被清空。
Query:
参数会显示在路径上,刷新不会被清空,name 可以使用path路径。
导航钩子是什么?它的参数是什么?
参数:有to(前往的路由)、from(离开的路由)、next(可以在beforeEach改变或中断导航)
全局钩子:
- router.beforeEach(to,from,next),next必须被调用 作用:跳转前进行判断拦截。
- router.aferEach(to,from) 没有next钩子,并不改变导航本身
组件内的钩子
- 配置路由时 beforeEnter,参数与全局钩子相同
单独路由独享组件
const Home = { template: `<div></div>`, beforeRouteEnter(to, from, next){ // 在渲染该组件的对应路由被 confirm 前调用 // 不能获取组件实例 ‘this’,因为当守卫执行前,组件实例还没被创建 }, beforeRouteUpdate(to, from, next){ // 在当前路由改变,但是该组件被复用时调用 // 例:对于一个动态参数的路径 /home/:id,在/home/1 和 /home/2 之间跳转的时候 // 由于会渲染同样的 Home 组件,因此组件实例会被复用,而这个钩子就会在这个情况下被调用。 // 可以访问组件实例 'this' }, beforeRouteLeave(to, from, next){ // 导航离开该组件的对应路由时调用 // 可以访问组件实例 'this' } }
keep-alive组件
keep-alive是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
include定义缓存白名单,keep-alive会缓存命中的组件;exclude定义缓存黑名单,被命中的组件将不会被缓存;max定义缓存组件上限,超出上限使用LRU(最久未使用内容)的策略置换缓存数据。
keep-alive一个场景的使用场景就是和router-view配合
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<keep-alive :include="cachedViews">
<router-view :key="key" />
</keep-alive>
</transition>
<Footer />
</section>
</template>
keep-alive原理
缓存列表
第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;
第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;
第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;
第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key);
第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。
patch阶段
- 在首次加载被包裹组件时,由
keep-alive.js
中的render
函数可知,vnode.componentInstance
的值是undefined
,keepAlive
的值是true
,因为keep-alive组件作为父组件,它的render
函数会先于被包裹组件执行;那么就只执行到i(vnode, false /* hydrating */)
,后面的逻辑不再执行; - 再次访问被包裹组件时,
vnode.componentInstance
的值就是已经缓存的组件实例,那么会执行insert(parentElm, vnode.elm, refElm)
逻辑,这样就直接把上一次的DOM插入到了父元素中。
- 在首次加载被包裹组件时,由
Vue 和 React
vue和react的相同之处
- 实现原理和流程基本一致,都是用virtual dom+diff算法
- 都是用组件化思想
- 都是响应式,推崇单向数据流
- 都支持服务端渲染
Vue和React通用流程:vue template/react jsx -> render函数 -> 生成VNode -> 当有变化时,新老VNode diff -> diff算法对比,并真正去更新真实DOM。
vue和react的差别
以我的经验来说
hook编写思维的区别,代码围绕状态来编写(不过Vue3也是这种形式了)
Vue是数据改变时自动通知ui改变,React需要手动去声明改动
组合代码方式不同,Vue是mixins,而React因为组件是函数,可以传入别的组件实现高阶组件HOC的效果
Vue是模板,React是jsx,jsx 可以被视作变量,这使得 React 中的模板更加自由得复用
监听数据的不同
vue可以通过getter、setter以及一些函数的劫持能够精确的知道数据变化,不需要特别的优化就能达到很好的性能
react默认是通过比较引用的方式进行的,如果不优化可能会导致大量不必要的VDOM的重新渲染
vue使用的是可变数据,而react更强调数据的不可变
...
网络、http
五层网络模型
(1)应用层
为应用程序提供网络访问服务及应用层协议存留的地方。例如,HTTP协议提供了Web文档的请求和传送,SMTP(邮件传送协议)提供了电子邮件的传送,还有DNS(域名解析协议)将http://202.108.22.5转换为对人友好的www.baidu.com。
(2)传输层
提供端到端的服务,即主机到主机。负责将应用层的报文向目的地传送,还要确保传输差错控制和流量控制。在因特网中,有两个传输协议,即TCP和UDP,可提供端到端的、可靠的或者不可靠的传输。
(3)网络层
网络层负责将数据报的分组从一台主机移动到另一台主机,具体功能包括寻址、路由选择、连接建立、保持和终止等。在发出传输请求的主机中的传输层向网络层递交传输报文段和目的地址,就像你通过快递服务寄件时提供目的地址一样。
(4)数据链路层
数据链路层最基本的服务就是将源自网络层的数据可靠地传输到相邻节点,下一个节点的目的主机的链路层将数据报上传给网络层。数据链路层的例子有以太网、WiFi等,该层需要实现的功能包括:物理地址寻址、数据的成帧、流量控制、数据的检错、重发等。
(5)物理层
物理层的任务是将该帧中的一个一个比特从一个节点向下一个节点移动。在物理层中的协议与链路相关,并且需要确保原始的数据可在各种物理媒体上传输。比如以太网的许多物理层协议有和双绞铜线、同轴电缆、光纤,等等相关。
http协议在应用层,tcp协议在传输层,ip协议在网络层。
TCP
TCP的三次握手和四次挥手
三次握手的必要性
为了服务端能够接收到客户端的信息并做出正确的应答而进行了前两次握手
为了客户端能够接收到服务端的信息并做出正确的应答而进行了后两次握手
标志说明
ACK: tcp规定,只有当ACK = 1时有效,也规定连接建立后所有发送的报文的ACK必须为1
SYN:在建立连接时用来同步序号,SYN置1时表示这是一个连接请求或连接接受报文
FIN:终结的意思,用来释放一个连接。当FIN = 1 时,表明此报文段的发送方的数据已经放松完毕,可以并请求释放连接
三次握手说明
第一次握手 :建立连接,客户端发送连接建立请求,将SYN置1,序列号seq为x。然后客户端进入SYN_SEND状态,等待服务器的确认
第二次握手 :服务器收到SYN报文段,收到后,ACK置1,确认号ack为x+1;同时自己也要发送SYN请求信息,将SYN置1,序列号seq为y;服务器将SYN+ACK报文段发送给客户端,进入SYN_RECV状态。
第三次握手 :客户端收到SYN+ACK报文段。收到后,将确认号ack为y+1;向服务器发送ACK报文段。这个报文段发送完毕之后,客户端和服务器都进入ESTABLISHED状态,完成三次握手,可以开始传数据。
第三次握手的时候,客户端已经处于ESTABLISHED
状态,并且已经能够确认服务器的接收、发送能力正常,这个时候相对安全了,可以携带数据。
四次挥手说明
第一次挥手 :主机1设置序列号seq为u,向主机2发送FIN报文段,此时,主机1变为FIN_WAIT状态,表明没有数据要发送给主机2了
第二次挥手 :主机2收到主机1发送的FIN报文段,向主机回复一个ACK报文段,确认号为u+1,序列号seq为v;主机2进入CLOCK_WAIT状态,主机1收到主机2的ACK报文段之后,变为FIN_WAIT_2状态。
第三次挥手 :主机2向主机1发送FIN报文段,设置序列号seq为w,确认号ack为u+1,请求关闭连接,同时主机2进入LAST_ACK状态
第四次挥手 :主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,确认号为w+1,序列号seq为u+1,然后主机1进入TIME_WAIT;主机2收到主机1的ACK报文段的时候,就关闭连接;此时主机1等待2MSL(报文最大的生存时间)后依旧没有收到回复,则证明服务器已经正常关闭,那主机1也可以关闭连接了
第一次挥手发出时,客户端就进入只能接收不能发送的状态;
四次握手必要性
第二次和第三次挥手不能合并,因为第二次代表服务段收到了关闭的请求,而它与第三次并不是连续的,可能服务段还有需要发送的数据,等待其全部发送完成,之后才发送第三次握手。
2MSL意义
- 1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
- 1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
半连接队列、全连接队列、SYN Flood攻击
半连接队列
当客户端发送SYN
到服务端,服务端收到以后回复ACK
和SYN
,状态由LISTEN
变为SYN_RCVD
,此时这个连接就被推入了SYN队列,也就是半连接队列。
全连接队列
当客户端返回ACK
, 服务端接收后,三次握手完成。这个时候连接等待被具体的应用取走,在被取走之前,它会被推入另外一个 TCP 维护的队列,也就是全连接队列(Accept Queue)。
SYN Flood 攻击原理
SYN Flood 属于典型的 DoS/DDoS 攻击。其攻击的原理很简单,就是用客户端在短时间内伪造大量不存在的 IP 地址,并向服务端疯狂发送SYN
。对于服务端而言,会产生两个危险的后果:
- 处理大量的
SYN
包并返回对应ACK
, 势必有大量连接处于SYN_RCVD
状态,从而占满整个半连接队列,无法处理正常的请求。 - 由于是不存在的 IP,服务端长时间收不到客户端的
ACK
,会导致服务端不断重发数据,直到耗尽服务端的资源。
应对 SYN Flood 攻击
- 增加 SYN 连接,也就是增加半连接队列的容量。
- 减少 SYN + ACK 重试次数,避免大量的超时重发。
- 利用 SYN Cookie 技术,在服务端接收到
SYN
后不立即分配连接资源,而是根据这个SYN
计算出一个Cookie,连同第二次握手回复给客户端,在客户端回复ACK
的时候带上这个Cookie
值,服务端验证 Cookie 合法之后才分配连接资源。
往返时延 RTT的计算
往返时延 RTT(Round-Trip Time)
TCP的计算方式是,通过时间戳的标记,计算发出包的时间与收到响应包的时间的差
- a 向 b 发送的时候,
timestamp
中存放的内容就是 a 主机发送时的内核时刻ta1
。 - b 向 a 回复 s2 报文的时候,
timestamp
中存放的是 b 主机的时刻tb
,timestamp echo
字段为从 s1 报文中解析出来的 ta1。 - a 收到 b 的 s2 报文之后,此时 a 主机的内核时刻是 ta2, 而在 s2 报文中的 timestamp echo 选项中可以得到
ta1
, 也就是 s2 对应的报文最初的发送时刻。然后直接采用 ta2 - ta1 就得到了 RTT 的值。
TCP超时重传 (RTO 超时重传时间)
TCP 具有超时重传机制,间隔一段时间没有等到数据包的回复时,重传这个数据包。
重传间隔也叫做超时重传时间(Retransmission TimeOut, 简称RTO)
经典方法
经典方法引入了一个新的概念——SRTT(Smoothed round trip time,即平滑往返时间),每产生一次新的 RTT. 就根据一定的算法对 SRTT 进行更新,具体而言,计算方式如下(SRTT 初始值为0):
SRTT = (α * SRTT) + ((1 - α) * RTT)
RTO = min(ubound, max(lbound, β * SRTT))
β 是加权因子,一般为1.3 ~ 2.0
, lbound 是下界,ubound 是上界。 α 的范围是0.8 ~ 0.9
TCP流量控制
对于发送端和接收端而言,TCP 需要把发送的数据放到发送缓存区, 将接收的数据放到接收缓存区。
而流量控制索要做的事情,就是在通过接收缓存区的大小,控制发送端的发送。如果对方的接收缓存区满了,就不能再继续发送了。
要具体理解流量控制,首先需要了解滑动窗口
的概念。
TCP 滑动窗口
TCP 滑动窗口分为两种: 发送窗口和接收窗口。
发送窗口
发送端的滑动窗口结构如下:
其中包含四大部分:
- 已发送且已确认
- 已发送但未确认
- 未发送但可以发送
- 未发送也不可以发送
其中有一些重要的概念
发送窗口就是图中被框住的范围。SND 即send
, WND 即window
, UNA 即unacknowledged
, 表示未被确认,NXT 即next
, 表示下一个发送的位置。
接收窗口
接收端的窗口结构如下:
REV 即 receive
,NXT 表示下一个接收的位置,WND 表示接收窗口大小。
具体过程
首先双方三次握手,初始化各自的窗口大小,均为 200 个字节。
发送端给接收端发送 100 个字节,发送端SND.NXT 右移 100 个字节,即 可用窗口
减少了 100 个字节。
100 个字节到达了接收端,放置在缓冲队列中。不过此时由于大量负载的原因,接收端处理不了这么多字节,只能处理 40 个字节,剩下的 60
个字节被留在了缓冲队列中。
所以接收端的策略是减小接收窗口并通知发送端:具体来说,缩小 60 个字节,由 200 个字节变成了 140 字节,因为缓冲队列还有 60 个字节没被应用拿走。
接收端会在 ACK 的报文首部带上缩小后的滑动窗口 140 字节,发送端对应地调整发送窗口的大小为 140 个字节。
此时对于发送端而言,已经确认了 40 字节, SND.UNA 右移 40 个字节,同时发送窗口缩小为 140 个字节。
TCP拥塞控制
拥塞控制需要处理在拥塞网络环境下,经常丢包,如何发包的问题
对于拥塞控制来说,TCP 每条连接都需要维护两个核心状态:
- 拥塞窗口(Congestion Window,cwnd)
- 慢启动阈值(Slow Start Threshold,ssthresh)
涉及到的算法有这几个:
- 慢启动
- 拥塞避免
- 快速重传和快速恢复
拥塞窗口
拥塞窗口(Congestion Window,cwnd)是指目前自己还能传输的数据量大小。
我们已知接收方的接收窗口会控制发送方的发送窗口大小
实际上,发送窗口大小是由发送方的拥塞窗口和接收方的接收窗口共同控制
发送窗口大小 = min(rwnd, cwnd)
取两者的较小值。而拥塞控制,就是来控制cwnd
的变化。
慢启动
刚开始进入传输数据的时候,你是不知道现在的网路到底是稳定还是拥堵的,如果做的太激进,发包太急,那么疯狂丢包,造成雪崩式的网络灾难。
因此,拥塞控制首先就是要采用一种保守的算法来慢慢地适应整个网路,这种算法叫慢启动
。运作过程如下:
- 首先,三次握手,双方宣告自己的接收窗口大小
- 双方初始化自己的拥塞窗口(cwnd)大小
- 在开始传输的一段时间,发送端每收到一个 ACK,拥塞窗口大小加 1,也就是说,每经过一个 RTT,cwnd 翻倍。如果说初始窗口为 10,那么第一轮 10 个报文传完且发送端收到 ACK 后,cwnd 变为 20,第二轮变为 40,第三轮变为 80,依次类推。
它不会一直呈指数增长下去,它的阈值叫做慢启动阈值,达到阈值后增长大幅减小。
拥塞避免
达到阈值后,每次cwnd增加幅度 : 1 / cwnd。一轮 RTT ,收到 cwnd 个 ACK, 最后拥塞窗口的大小 cwnd 总共才增加 1。
快速重传和快速恢复
快速重传及选择性重传
在 TCP 传输的过程中,如果发生了丢包,即接收端发现数据段不是按序到达的时候,接收端的处理是重复发送之前的 ACK。
比如第 5 个包丢了,即使第 6、7 个包到达的接收端,接收端也一律返回第 4 个包的 ACK。当发送端收到 3 个重复的 ACK 时,意识到丢包了,于是马上进行重传,不用等到一个 RTO 的时间到了才重传。
这就是快速重传,它解决的是是否需要重传的问题。
在收到发送端的报文后,接收端回复一个 ACK 报文,那么在这个报文首部的可选项中,就可以加上SACK
这个属性,通过left edge
和right edge
告知发送端已经收到了哪些区间的数据报。因此,即使第 5 个包丢包了,当收到第 6、7 个包之后,接收端依然会告诉发送端,这两个包到了。剩下第 5 个包没到,就重传这个包。
这个过程也叫做选择性重传(SACK,Selective Acknowledgment),它解决的是如何重传的问题。
快速恢复
当然,发送端收到三次重复 ACK 之后,发现丢包,觉得现在的网络已经有些拥塞了,自己会进入快速恢复阶段。
在这个阶段,发送端如下改变:
- 拥塞阈值降低为 cwnd 的一半
- cwnd 的大小变为拥塞阈值
- cwnd 线性增加
以上就是 TCP 拥塞控制的经典算法: 慢启动、拥塞避免、快速重传和快速恢复。
TCP 和 UDP 的区别
UDP提供不可靠服务,具有TCP所没有的优势:
UDP无连接,时间上不存在建立连接需要的时延。空间上,TCP需要在端系统中维护连接状态,需要一定的开销。 此连接装入包括接收和发送缓存,拥塞控制参数和序号与确认号的参数。UCP不维护连接状态,也不跟踪这些参数,开销小。空间和时间上都具有优势。 DNS如果运行 UDP 上,不需要建立连接速度快很多; HTTP使用 TCP,是因为对于基于文本数据的Web网页来说,可靠性很重要。 同一种专用应用服务器在支持UDP时,一定能支持更多的活动客户机。
分组首部开销小,TCP首部20字节,UDP首部8字节。
UDP没有拥塞控制 ,应用层能够更好的控制要发送的数据和发送时间,网络中的拥塞控制也不会影响主机的发送速率。某些实时应用要求以稳定的速度发送,能容 忍一些数据的丢失,但是不能允许有较大的时延(比如实时视频,直播等)
UDP提供尽最大努力的交付,不保证可靠交付。 所有维护传输可靠性的工作需要用户在应用层来完成。没有TCP的确认机制、重传机制 。如果因为网络原因没有传送到对端,UDP也不会给应用层返回错误信息
UDP是面向报文的,对应用层交下来的报文,添加首部后直接乡下交付为IP层,既不合并,也不拆分,保留这些报文的边界。 对IP层交上来UDP用户数据报,在去除首部后就原封不动地交付给上层应用进程,报文不可分割,是UDP数据报处理的最小单位。 正是因为这样,UDP显得不够灵活,不能控制读写数据的次数和数量。比如我们要发送100个字节的报文,我们调用一次sendto函数就会发送100字节,对端也需要用recvfrom函数一次性接收100字节,不能使用循环每次获取10个字节,获取十次这样的做法。
UDP常用一次性传输比较少量数据的网络应用,如DNS,SNMP等,因为对于这些应用,若是采用TCP,为连接的创建,维护和拆除带来不小的开销。UDP也常用于多媒体应用(如IP电话,实时视频会议,流媒体等)数据的可靠传输对他们而言并不重要,TCP的拥塞控制会使他们有较大的延迟,也是不可容忍的
TCP 是有状态的吗 ? HTTP 呢?
状态是指请求之间的影响;
无状态是每个请求都是相互独立的, 接收方对发送方之前的请求没有记忆, 只根据报文中的内容进行处理;
有状态是请求之间会互相影响, 接收方会对发送方之前的请求进行标识等操作, 再结合接收报文到内容再进行处理;
- TCP 是 有状态的 , 如: 建立连接和挥手有着状态的改变, 报文的序列号, 重传等机制
- UDP 是 无状态的 , UDP 不需要通信前建立连接, 也没有确认和重传等机制, 只根据接收到 UDP 报文进行处理
- HTTP 是 无状态的 , HTTP 中每个请求也是独立的, 所以需要在报文内携带相关状态的记录, 如 cookie
TCP是全双工吗?
- 单工: 一方固定是发送方, 一方固定是接收方,单向传输
- 半双工: 某一时刻,一方只能作发送或接收方, 数据只能是往接收方传输的
- 全双工: 一方既可以是发送方又可以是接收方, 数据可以同时在两个方向上传输
TCP 是全双工的, 一方主机可以同时发送和接收数据
Http
一次完整的HTTP事物流程
- 域名解析
- 发起TCP的三次握手
- 建立TCP连接并发起HTTP请求(如果是HTTPS)
- 服务器响应HTTP请求,浏览器得到HTML代码
- 浏览器解析HTML代码,并请求HTML中的资源
- 浏览器渲染页面呈现给用户
- 结束连接
HTTP头
- 请求行:方法,URL字段和http版本
- 通用头:Cache-Control、Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via
- 实体头:Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header
- 请求头:Accept、Accept-Charset、Accept-Encoding、Accept-Language、Authorization、Expect、From、Host、If-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since、Max-Forwards、Proxy-Authorization、RangeReferer、TE、User-Agent
- 响应头:Accept-Ranges、Age、ETag、Location、Referer、Retry-After Server、Vary、www-Authenticate
HTTP状态码
概念:
当用户访问网页时,浏览器会向网页所在的服务器发起请求,网页所在服务器会返回一个包含HTTP状态码的信息头以响应浏览器的请求,HTTP状态码用来描述服务器对请求的处理结果。
HTTP状态码分类
- 1**:信息,服务器接收到请求后,需要请求者继续执行操作
- 2**:成功,操作成功被接收并处理
- 3**:重定向,需要进一步的操作以完成请求
- 4**:客户端错误,请求包含语法错误或无法完成请求
- 5**:服务器错误,服务器在请求的过程发生了错误
常见的HTTP状态码
- 200——OK,请求成功
- 301——Moved Permanently,资源(网页等)被永久转移到其他URL
- 302——Found,307——Temporary Redirect,临时重定向,请求的文档被临时移动到别处
- 304——Not Modified,未修改,表示客户端缓存的版本是最近的
- 401——Unauthorized,请求要求用户的身份认证
- 403——Forbidden,禁止,服务器理解客户端请求,但是拒绝处理此请求,通常是权限设置所致
- 404——Not Found,请求的资源(网页等)不存在
- 500——Internal Server Error——内部服务器错误
- 502——Bad Gateway,充当网关或代理的服务器从远端服务器接收到了一个无效的请求
- 504——Gateway Time-out,充当网关或代理的服务器,未及时从远端服务器获取请求
GET 和 POST 有什么区别?
首先最直观的是语义上的区别。
而后又有这样一些具体的差别:
- 从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
- 从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
- 从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
- 从幂等性的角度,
GET
是幂等的,而POST
不是。(幂等
表示执行相同的操作,结果也是相同的) - 从TCP的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
缓存
强缓存
强缓存是浏览器不向服务端发送请求而使用缓存的策略
如果强缓存命中了,浏览器不会向服务端发送请求,而直接拟造一个状态码 200
的响应
强缓存是利用http头中的Expires
和Cache-Control
两个字段来控制的,用来表示资源的缓存时间
- Expires: 是http1.0的规范,它的值是一个绝对时间的GMT格式的时间字符串。比如网页的
Expires
值是:
expires:Mar, 06 Apr 2020 10:47:02 GMT。
这个时间代表这这个资源的失效时间,只要发送请求时间是在Expires
之前,那么本地缓存始终有效,则在缓存中读取数据。所以这种方式有一个明显的缺点,由于失效的时间是一个绝对时间,所以当服务器与客户端时间偏差较大时,就会导致缓存混乱。
- Cache-Control: 是http1.1中出现的,一般利用该字段的max-age来判断,这个值是一个相 对时间。例如:
Cache-Control:max-age=3600 // 代表着资源的有效期是3600秒
除了该字段还有其他的几个常用的值。
- no-cache:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载。
- no-store:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源。
- public:可以被所有的用户缓存,包括终端用户和CDN等中间代理服务器。
- private:只能被终端用户的浏览器缓存,不允许CDN等中继缓存服务器对其缓存。
Cache-Control与Expires的优先级
Cache-Control与Expires可以在服务端配置同时启用,同时启用的时候Cache-Control优先级高。比如:
cache-control:max-age=691200
expires:Fri, 06 Mar 2020 10:47:02 GMT
那么表示资源可以被缓存的最长时间为691200秒,会优先考虑max-age
。
协商缓存
如果浏览器未命中强缓存,就进入协商缓存
协商缓存是由服务器确定缓存资源是否可用。主要涉及到两组header字段:
Etag`和`If-None-Match
Last-Modified`和`If-Modified-Since
如果缓存资源可用,服务端返回状态码 304
的响应
Last-Modify/If-Modify-Since
浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间,例如Last-Modify: Thu,31 Dec 2037 23:59:59 GMT。
当浏览器再次请求该资源时,request的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。如果命中缓存,则返回304,并且不会返回资源内容,并且不会返回Last-Modify。
Etag/If-None-Match
从上面看可能会觉得使用Last-Modified已经足以让浏览器知道本地的缓存副本是否足够新,为什么还需要Etag呢?
首先,Etag/If-None-Match 是“实体标签”(Entity Tag)的缩写,是资源的一个唯一标识,资源变化都会导致ETag变化。服务器根据浏览器上送的If-None-Match值来判断是否命中缓存。
因此,Etag的出现主要是为了解决几个Last-Modified比较难解决的问题:
- 一些文件也许会周期性的更改,但是他的内容并不改变(仅仅改变的修改时间),这个时候我们并不希望客户端认为这个文件被修改了,而重新GET;
- 某些文件修改非常频繁,比如在秒以下的时间内进行修改,(比方说1s内修改了N次),If-Modified-Since能检查到的粒度是s级的,这种修改无法判断(或者说UNIX记录MTIME只能精确到秒);
- 某些服务器不能精确的得到文件的最后修改时间。
Last-Modified与ETag是可以一起使用的,服务器会优先验证ETag,一致的情况下,才会继续比对Last-Modified,最后才决定是否返回304。
跨域
跨域的条件:协议,域名,端口不同,
跨域请求分为简单请求和非简单请求
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
简单请求
服务端需要设置3个字段
如
Access-Control-Allow-Origin: http://api.bob.com 域名,*时表示接收任何域名
Access-Control-Allow-Credentials: true 允许携带cookie
Access-Control-Expose-Headers: FooBar 指定的额外字段
withCredentials
必要条件
指定
Access-Control-Allow-Credentials
字段Access-Control-Allow-Credentials: true
开发者必须在AJAX请求中打开
withCredentials
属性。var xhr = new XMLHttpRequest(); xhr.withCredentials = true;
Access-Control-Allow-Origin
就不能设为*
Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie
也无法读取服务器域名下的Cookie。
非简单请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。
"预检"请求用的请求方法是OPTIONS
,表示这个请求是用来询问的。头信息里面,关键字段是Origin
,表示请求来自哪个源。
除了Origin
字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
。
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
。
响应
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers
字段,则Access-Control-Allow-Headers
字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
解决跨域
CORS
普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。
axios.defaults.withCredentials = true
// 允许跨域访问的域名:若有端口需写全(协议+域名+端口),若没有端口末尾不用加'/'
response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");
// 允许前端带认证cookie:启用此项后,上面的域名不能为'*',必须指定具体的域名,否则浏览器会提示
response.setHeader("Access-Control-Allow-Credentials", "true");
// 提示OPTIONS预检时,后端需要设置的两个常用自定义头
response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
JSONP
利用 script 标签没有跨域的限制,配合服务器的接口带上参数(为全局函数的变量名),动态生成 script 代码返回给前端并在script标签中运行
- script代码:调用全局函数(调用的函数由参数获得)并传入对象
1.)原生实现:
<script>
var script = document.createElement('script');
script.type = 'text/javascript';
// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
document.head.appendChild(script);
// 回调执行函数
function handleCallback(res) {
alert(JSON.stringify(res));
}
</script>
服务端返回如下(返回时即执行全局函数):
handleCallback({"status": true, "user": "admin"})
webpack反向代理
开发环境可以使用反向代理快速解决跨域问题
原理:
- 跨域是浏览器的限制,本地服务器代理目的服务器响应可以绕过跨域的限制
- webpack 开启了本地服务器,设置代理后同时也是代理服务器;向本地服务器发送请求时,符合同源政策。
反向代理和正向代理
反向代理,代理服务器代理服务器响应;正向代理,代理服务器代理客户端请求
反向代理,客户端认为代理服务器就是目的服务器,对代理服务器代理了什么服务器并不知情;
正向代理,服务端认为代理服务器就是初始发送方,是否告知服务器被代理的客户端信息由代理服务器决定。
反向代理例:用户访问 http://www.test.com/readme
,但www.test.com
上并不存在readme页面,他是偷偷从另外一台服务器上取回来,然后作为自己的内容返回用户,但用户并不知情。这里所提到的 www.test.com
这个域名对应的服务器就设置了反向代理功能。
正向代理例:vpn、加速器等等
Cookie和Session
cookie和session是什么?他们有什么联系?他们有什么区别?
cookie采用的是客户端的会话状态
的一种储存机制。它是服务器在本地机器上存储的小段文本或者是内存中的一段数据,并随每一个请求发送至同一个服务器。
session是一种服务器端的信息管理机制
,它把这些文件信息以文件的形式存放在服务器的硬盘空间上。(这是默认情况,可以用memcache把这种数据放到内存里面)。
当客户端向服务器发出请求时,要求服务器端产生一个session时,服务器端会先检查一下,客户端的cookie里面有没有session_id,是否过期。如果有这样的session_id的话,服务器端会根据cookie里的session_id把服务器的session检索出来。如果没有这样的session_id的话,服务器端会重新建立一个。PHPSESSID是一串加了密的字符串,它的生成按照一定的规则来执行。同一客户端启动二次session_start的话,session_id是不一样的。
区别:Cookie保存在客户端浏览器中,而Session保存在服务器上。Cookie机制是通过检查客户身上的“通行证”来确定客户身份的话,那么Session机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。
Set-Cookie 和 Cookie 标头
属性 | 说明 |
---|---|
NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) |
expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) |
path=PATH | 将服务器上的文件目录作为Cookie的适用对象(若不指定则默认为文档所在的文件目录) |
domin=域名 | 作为 Cookie 适用对象的域名 (若不指定则 默认为创建 Cookie 的服务器的域名) |
Secure | 仅在 HTTPS 安全通信时才会发送 Cookie |
HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 |
SameSite | 可选值:Strict,Lax,None; |
如果不设置expires,这个cookie就是会话cookie,它会被储存到内存,客户端关闭会被删除;
path和domin用于设置cookie的作用域;
cookie 也包含在子域名里,如
Domain=mozilla.org
,developer.mozilla.org
也会带上cookie
,域名的左边是最低级域名。cookie 也包含在子路径里,如
Path=/docs
,/docs/Web/HTTP
也会带上 cookieHttpOnly禁止 JavaScript读取 cookie,防止cookie被窃取,进行跨站脚本攻击(XSS)
设置SameSite可以防止跨站伪造请求(XSRF)
Strict 为完全禁止 cookie 第三方携带;Lax 仅允许 链接,预加载请求,GET 表单 第三方携带;而 None 不做限制,但需设置 Secure 才能生效。 Cookie 的 Samesite 属性
Https
Https是什么?
https就是http下加入SSL层,它建立了一个安全的信息传输通道,对信息进行加密,确保数据的真实性、完整性
Https协议的工作原理
客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
- 客户使用https url访问服务器,则要求web 服务器建立ssl链接。
- web服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),返回或者说传输给客户端。
- 客户端和web服务器端开始协商SSL链接的安全等级,也就是加密等级。
- 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站。
- web服务器通过自己的私钥解密出会话密钥。
- web服务器通过会话密钥加密与客户端之间的通信。
采用公钥密钥加密会话密钥,会话密钥是对称加密密钥
要求建立 ssl -> 服务端发送证书 -> 浏览器验证证书 -> 协商参数和密钥制度等 -> 客户端发送公钥加密的会话密钥密文 -> 客户端用私钥解密获取会话密钥 -> 开始通信,http报文使用会话密钥进行加密
Client Hello:客户端发送一条 Hello 信息发起握手,消息包含客户端要使用的 TLS 协议版本、Client random、Cipher suite、以及一些其他的必要信息
Server Hello:服务端收到客户端的 Hello 信息,根据信息中提供的加密套件列表,决定最终的握手类型。最后返回给客户端一条 Server Hello 消息,消息包含服务端随机数、最终确定使用的加密套件列表、以及服务端的证书(如 HTTPS 证书,证书中包含 RSA 生成的公钥和服务器的域名)
客户端验证证书,生成预主密钥:Server Hello 送来的证书经验证证书可信且站点归属正确后,客户端会将 Client random 与 Server random 结合,使用伪随机函数(Pseudorandom Function)生成 Pre-master secret,并使用证书中的公钥加密预主密钥,将其发送到服务端
服务端使用私钥解密,获取预主密钥:服务端接收到加密后的 Pre-master secret,使用之前 RSA 生成的与公钥配对的私钥进行解密,获得与客户端相同的 Pre-master secret
生成 Session key:此时客户端与服务端都通过密钥交换的过程,得到了相同的 Client random、Server random 与 Pre-master secret,客户端与服务端便可以各自推导出相同的 Session key,后续的通信内容则会使用这个 Session key 作为 AES_128_GCM 的密钥进行加解密
客户端与服务端交换加密的握手结束消息,并进行 HMAC 验证:生成会话密钥(Session key)之后,客户端与服务端会使用 Session key 加密消息体,交换一次 Finish 消息,表示握手正式完成,确认双方都能正常的使用 Session key 进行加密的数据传输,同时在 Finish 消息中使用 HMAC 进行数字签名验证以验证握手阶段的完整性
浏览器如何验证证书?
证书链
CA下发给网站的证书都是一个证书链,也就是一层一层的证书,从根证书开始,到下级CA,一层一层,最后一层就是网站证书。
为什么是证书链呢?因为证书是一级一级颁发的,网站证书是由下级 CA 颁发,下级 CA 的证书又由区域级 CA 颁发,这样一层一层最后到根证书。
浏览器要验证证书,就需要验证每一级的证书的真实性,某一级的证书由上一级证书来保证真实性。
证书内容
证书的格式是有规定的 ,一般是 X509v3
协议,可以区分大致三部分,
- 证书签名
- CA的公钥
- 其他保证证书有效性的信息
X.509v3证书由三部分组成:
tbsCertificate (to be signed certificate),待签名证书。 SignatureAlgorithm,签名算法。 SignatureValue,签名值。 tbsCertificate又包含10项内容,在HTTPS握手过程中以明文方式传输:
Version Number,版本号。 Serial Number,序列号。 Signature Algorithm ID,签名算法ID。 Issuer Name,发行者。 Validity period,有效时间。 Subject name ,证书主体名称。 Subject Public Key Info ,证书主体公钥信息,包含公钥算法和公钥值。 Issuer Unique Identifier (optional),发行商唯一ID。 Subject Unique Identifier (optional),主体唯一ID。 Extensions (optional),扩展。
签名和验证
签名依赖公钥密码制度;公钥密码有一个加密钥,一个解密钥,加密钥加密只有解密钥才能解密。
在上面 ssl 建立中,我们知道,客户端用服务器返回的证书里的公钥进行加密会话密钥,然后发送加密后的密文给服务端,服务端用私钥解密;此时加密钥是公钥,解密钥是私钥
而在签名时,签名颁发者用加密钥对 证书信息 加密生成密文,这个密文就是签名,然后证书里带上 签名 + 证书信息 + 解密钥;此时加密钥是私钥,解密钥是公钥
浏览器在收到证书后,用公钥对签名进行解密,因为 只有用私钥加密后的签名才能用公钥解密,所以它可以保证证书的真实性;同时校验保证有效性的信息,比如 证书是否过期,颁发的相关信息是否一致,颁发机构是否被废除等
浏览器就这样用上一级的公钥一层层地验证证书,最后验证到了根证书。最高层的根证书如何验证呢?
最高层的根证书是自签名的,也就是自己颁发给自己,所以它的公钥不仅用来解密下层的签名,也用来给自己的签名解密。
这个根证书是验证证书链中的最后一环,但它不是储存在证书链中;根证书储存在操作系统或浏览器内置的可信根证书内中;因为根证书储存在内部,所以它的真实性由浏览器背书。
HTTP2
二进制分帧
帧:HTTP/2 数据通信的最小单位消息:指 HTTP/2 中逻辑上的 HTTP 消息。 例如请求和响应等,消息由一个或多个帧组成。
流:存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数ID。
HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。 HTTP / 1 的请求和响应报文,都是由起始行,首部和实体正文(可选)组成,各部分之间以文本换行符分隔。HTTP/2 将请求和响应数据分割为更小的帧,并且它们采用二进制编码。
HTTP/2 中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流。 每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装。
多路复用
在 HTTP/2 中,有了二进制分帧之后,HTTP /2 不再依赖 TCP 链接去实现多流并行了,在 HTTP/2中:
- 同域名下所有通信都在单个连接上完成。
- 单个连接可以承载任意数量的双向数据流。
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装。
这一特性,使性能有了极大提升:
- 同个域名只需要占用一个 TCP 连接,消除了因多个 TCP 连接而带来的延时和内存消耗。
- 单个连接上可以并行交错的请求和响应,之间互不干扰。
- 在HTTP/2中,每个请求都可以带一个31bit的优先值,0表示最高优先级, 数值越大优先级越低。有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。
头部压缩
HTTP 1.1请求的大小变得越来越大,有时甚至会大于TCP窗口的初始大小,因为它们需要等待带着ACK的响应回来以后才能继续被发送。HTTP/2对消息头采用HPACK(专为http/2头部设计的压缩格式)进行压缩传输,能够节省消息头占用的网络的流量。而HTTP/1.x每次请求,都会携带大量冗余头信息,浪费了很多带宽资源。
- HTTP/2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;
- 首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
- 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。
服务器推送
服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。
服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。
DNS解析过程
浏览器缓存 -> 系统hosts表 -> LDS -> Root DNS-> gTLD -> Name Server
当一个用户在地址栏输入www.taobao.com时,DNS解析有大致十个过程,如下:
浏览器先检查自身缓存中有没有被解析过的这个域名对应的ip地址,如果有,解析结束。同时域名被缓存的时间也可通过TTL属性来设置。
如果浏览器缓存中没有(专业点叫还没命中),浏览器会检查操作系统缓存中有没有对应的已解析过的结果。而操作系统也有一个域名解析的过程。在windows中可通过c盘里一个叫hosts的文件来设置,如果你在这里指定了一个域名对应的ip地址,那浏览器会首先使用这个ip地址。但是这种操作系统级别的域名解析规程也被很多黑客利用,通过修改你的hosts文件里的内容把特定的域名解析到他指定的ip地址上,造成所谓的域名劫持。所以在windows7中将hosts文件设置成了readonly,防止被恶意篡改。
如果至此还没有命中域名,才会真正的请求本地域名服务器(LDNS)来解析这个域名,这台服务器一般在你的城市的某个角落,距离你不会很远,并且这台服务器的性能都很好,一般都会缓存域名解析结果,大约80%的域名解析到这里就完成了。
如果LDNS仍然没有命中,就直接跳到Root Server 域名服务器请求解析
根域名服务器返回给LDNS一个所查询域的主域名服务器(gTLD Server,国际顶尖域名服务器,如.com .cn .org等)地址
此时LDNS再发送请求给上一步返回的gTLD
接受请求的gTLD查找并返回这个域名对应的Name Server的地址,这个Name Server就是网站注册的域名服务器
Name Server根据映射关系表找到目标ip,返回给LDNS
LDNS缓存这个域名和对应的ip
LDNS把解析的结果返回给用户,用户根据TTL值缓存到本地系统缓存中,域名解析过程至此结束
CND
CDN是什么
CDN是内容分发网络,它利用了http的缓存机制,代理源站相应客户端给的请求。
- 内容储存,分发技术
- 负载均衡
- 就近获取内容
CDN的原理
- dns
- 负载均衡
- 缓存
- 首先经过本地的dns解析,请求cname指向的那台
cdn专用的dns服务器
。 - dns服务器返回全局负载均衡的服务器ip给用户
- 用户请求
全局负载均衡服务器
,服务器根据ip返回所在区域的负载均衡服务器ip给用户 - 用户请求
区域负载均衡服务器
,负载均衡服务器根据用户ip选择距离近的,并且存在用户所需内容的,负载比较合适的一台缓存服务器ip给用户。当没有对应内容的时候,会去上一级缓存服务器去找,直到找到资源所在的源站服务器,并且缓存在缓存服务器中。用户下一次在请求该资源,就可以就近拿缓存了。
前端安全
XSS (cross site scripting 跨站脚本注入)
XSS 攻击是指浏览器中执行恶意脚本(无论是跨域还是同域),从而拿到用户的信息并进行操作。三种形式:储存型、反射型、文档型
储存型
服务端将恶意代码储存在服务器上,然后在客户端执行这些代码。
经典场景:留言评论区提交一段脚本代码,前后端没有转义,评论内容存到了数据库,在页面渲染过程中直接执行
, 相当于执行一段未知逻辑的 JS 代码。这段JS代码逻辑未知,可能盗取cookie,弹出广告或更严重的攻击。
反射型
反射型XSS
指的是恶意脚本作为网络请求的一部分。攻击者观察请求,找到某个接口的返回值将会被插入dom且与
参数有关,如
http://juejin.com?q=<script>alert("game over")</script>
q参数被插入dom后浏览器如果不做处理,浏览器就会直接执行。
存储型
不一样的是,服务器并不会存储这些恶意脚本。
文档型
文档型的 XSS 攻击并不会经过服务端,而是作为中间人的角色,在数据传输过程劫持到网络数据包,然后修改里面的 html 文档。
这样的劫持方式包括WIFI路由器劫持
或者本地恶意软件
等
防范
不能相信用户输入,客户端和服务端都一定要对用户上传字符进行转义
开启Cookie httpOnly属性可以防范 js 读取cookie
CSP 浏览器的内容安全策略
Content Security Policy(简称CSP)浏览器内容策略的使用
CSP 指的是内容安全策略,它的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行。我们只需要配置规则,如何拦截由浏览器自己来实现。
通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式
<meta http-equiv="Content-Security-Policy">
CSRF( Cross-site request forgery 跨站伪造请求)
攻击者诱导用户点开他的网页,利用用户在某个网站已经登录的状态,向这个网站发送跨站请求
比如 <img src="https://xxx.com/info?user=hhh&count=100">
跨域政策不会拦截 img、form、script标签的请求,请求会自动带上关于 xxx.com 的 cookie 信息(这里是假定你已经在 xxx.com 中登录过)。
假如服务器端没有相应的验证机制,它可能认为发请求的是一个正常的用户,因为携带了相应的 cookie,可能会进行各种恶意操作。
防范
Cookie 的 SameSite 设置值 Strict 时,第三方向站点发送请求时,完全禁止请求携带站点的cookie
CSRF token
浏览器向服务器发送请求时,服务器生成一个字符串,将其植入到返回的页面中。
然后浏览器如果要发送请求,就必须带上这个字符串,然后服务器来验证是否合法,如果不合法则不予响应。这个字符串也就是
CSRF Token
,通常第三方站点无法拿到这个 token, 因此也就是被服务器给拒绝。
浏览器
在浏览器里,从输入 URL 到页面展示,这中间发生了什么
1. 用户输入url并回车
2. 浏览器进程检查url,组装协议,构成完整的url 3. 浏览器进程通过进程间通信(IPC)把url请求发送给网络进程 4. 网络进程接收到url请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程 5. 如果没有,网络进程向web服务器发起http请求(网络请求),请求流程如下: 5.1 进行DNS解析,获取服务器ip地址,端口 5.2 利用ip地址和服务器建立tcp连接 5.3 构建请求头信息 5.4 发送请求头信息 5.5 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容 6. 网络进程解析响应流程; 6.1 检查状态码,如果是301/302,则需要重定向,从Location自动中读取地址,重新进行第4步 如果是200,则继续处理请求。 6.2 200响应处理: 检查响应类型Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行 后续的渲染,如果是html则通知浏览器进程准备渲染进程准备进行渲染。 7. 准备渲染进程 7.1 浏览器进程检查当前url是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程 8. 传输数据、更新状态 8.1 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道” 8.2 渲染进程接收完数据后,向浏览器发送“确认提交” 8.3 浏览器进程接收到确认消息后更新浏览器界面状态:安全、地址栏url、前进后退的历史状态、更新web页面。 9. 接收到 html 文档后解析页面...
可以详细讲的知识点:页面四进程、IPC、缓存、DNS解析、CND、TCP建立、HTTP请求、SSL连接的建立(如果是HTTPS)、HTTP响应、解析页面
解析页面
- 渲染流程(上):HTML、CSS和JavaScript是如何变成页面的
- 渲染流程(下):HTML、CSS和JavaScript是如何变成页面的
- 分层和合成机制:为什么css动画比JavaScript高效
html解析
在渲染引擎内部,有一个叫 HTML 解析器(HTMLParser)的模块,它的职责就是负责将 HTML 字节流转换为 DOM 结构。
解析分为三个阶段
字节流转为token
将 token 是标签的标志,如同括号匹配,不同的开始标签的 token 类似不同类型的左括号
匹配
添入
随着标签 token 的不断匹配成功,不断有节点添入DOM树中
css解析
css来源:
- 通过 link 引用的外部 CSS 文件
<style>
- 标记内的 CSS元素的 style
- 属性内嵌的 CSS
CSS内容浏览器同样也不理解,需要被转化为浏览器能给个理解的结构 -- styleSheets
样式计算
styleSheets
生成后,需要计算出 DOM 节点中每个元素的具体样式。
转换样式表中的属性值,使其标准化
css 中有很多同义语法,如 blue 与 rgba(0,0,255);标准化就是将所有值转换为渲染引擎容易理解的、标准化的计算值
计算出 DOM 树中每个节点的具体样式
继承规则
层叠规则
这个阶段最终输出的内容是每个 DOM 节点的样式,并被保存在 ComputedStyle
的结构内。
创建布局树
结束样式计算后,需要计算出 DOM 树中可见元素的几何位置,这个计算过程叫做布局。
- 遍历 DOM 树中的所有可见节点,并把这些节点加到布局树中;
- 而不可见的节点会被布局树忽略掉,如 head 标签下面的全部内容,再比如 body.p.span 这个元素,因为它的属性包含 dispaly:none,所以这个元素也没有被包进布局树
布局计算
创建好布局树后,就要进行布局计算了。
- 生成图层树
- 为每一层生成绘制列表
图层树(RenderLayer Tree)
关于渲染层合成层等,更详细请看
- 高性能Web动画和渲染原理系列(2)——渲染管线和CPU渲染
- 高性能Web动画和渲染原理系列(3)——transform和opacity为什么高性能
- 高性能Web动画和渲染原理系列(5)合成层的生成条件和陷阱
- 浏览器渲染流程&Composite(渲染层合并)简单总结
一个渲染层的创建?
- 拥有层叠上下文
- 需要裁剪的元素
层叠上下文
拥有层叠上下文的元素会被单独提为一层,只有在层叠上下文上设置 z-index 才有效
BFC一定会创建层叠上下文
条件如下(仅列出常见 css 属性):
- 文档根元素(
<html>
); position
为absolute
relative
,z-index
值不为auto
的元素;position
值为fixed
或sticky
(粘滞定位)的元素- flex 容器的子元素,且
z-index
值不为auto
;grid (grid
) 容器的子元素,且z-index
值不为auto
; opacity
属性值小于1
的元素;transform、filter
不为none的元素
在层叠上下文中,子元素同样也按照上面解释的规则进行层叠。 重要的是,其子级层叠上下文的 z-index
值只在父级中才有意义。子级层叠上下文被自动视为父级层叠上下文的一个独立单元。
总结:
- 层叠上下文可以包含在其他层叠上下文中,并且一起创建一个层叠上下文的层级。
- 每个层叠上下文都完全独立于它的兄弟元素:当处理层叠时只考虑子元素。
- 每个层叠上下文都是自包含的:当一个元素的内容发生层叠后,该元素将被作为整体在父级层叠上下文中按顺序进行层叠。
裁剪
<style> div {
width: 200;
height: 200;
overflow:auto;
background: gray;
} </style>
<body>
<div >
<p>所以元素有了层叠上下文的属性或者需要被剪裁,那么就会被提升成为单独一层,你可以参看下图:</p>
<p>从上图我们可以看到,document层上有A和B层,而B层之上又有两个图层。这些图层组织在一起也是一颗树状结构。</p>
<p>图层树是基于布局树来创建的,为了找出哪些元素需要在哪些层中,渲染引擎会遍历布局树来创建层树(Update LayerTree)。</p>
</div>
</body>
出现这种裁剪情况的时候,渲染引擎会为文字部分单独创建一个层,如果出现滚动条,滚动条也会被提升为单独的层。
合成层
合成层是由渲染层提升而来,合成层的处理是依赖于硬件加速(即 GPU)。
没有提升为合成层的渲染层,与最近的祖先合成层同属一个合成层。
它分为 显示提升 和 隐式提升
显示提升
符合以下条件会被提升为合成层
- 具有
CSS3D
属性或CSS
透视效果 - 使用了
CSS
透明效果或CSS
变形动画 - 使用了硬件加速的
CSS Filters
技术 - 使用了剪裁
Clip
或者反射Reflection
,并且它的后代中包含一个合成层 - 拥有一个Z坐标比自己小的兄弟节点,且该节点是一个合成层。
隐式提升
RenderLayer
满足特殊条件时被提升为CompositingLayer
对开发者而言是比较可控的。但除此之外,在浏览器的合成阶段,还存在隐式合成的状况,一些特定的场景中出现的合成层并不是开发者主观期望的。
隐式合成主要发生在元素出现重叠时,层级较低的元素如果被提升为合成层后,最终合成的结果就可能出现在原来比自己层级更高的元素之上,从而出现错误的堆叠关系,为了纠正这种关系,只能让原本层级高(但是并不用提升为合成层
的元素)发生提升也成为合成层。
这个隐式合成其实隐藏着巨大的风险,如果在一个大型应用中,当一个z-index
比较低的元素被提升为单独图层之后,层叠在它上面的的元素统统都会被提升为单独的图层,可能会增加上千个图层,大大增加内存的压力,甚至直接让页面崩溃。这就是层爆炸的原理。
在Chrome
调试面板的Layers
功能中对分层相关的结果进行检视,查看哪些层进行了提升以及被提升的具体原因,避免出现与自己意图相悖的层提升:
绘制指令列表
每一个图层都会生成绘制指令列表并交由合成线程完成后面的工作
合成线程
合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
一般页面比视口大,合成线程不会绘制整个页面,而是将页面分块,优先选择视口近的图块绘制;块的大小一般不会特别大,通常是 256 * 256 或者 512 * 512 这个规格。
渲染进程中专门维护了一个栅格化线程池,专门负责把图块转换为位图。
合成线程会选择视口附近的图块,把它交给栅格化线程池生成位图。
生成位图的过程使用 GPU 加速,位图最后发送给合成线程。
合成线程发送绘制图块命令 DrawQuad 给浏览器进程。
浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。
回流、重绘和布局?
一帧的生成方式有,重排、重绘和合成三种方式;这三者前者都蕴含后者:重排一定重绘,重绘一定合成。
- 重排:修改了元素的宽高,定位等,导致需要重新生成布局树
- 重绘:改变了元素的颜色,背景颜色等,导致需要重新生成绘制列表
- 合成: CSS3 的
transform
、opacity
、filter
属性就可以实现合成的效果,它们既不要重排又无需重绘,所以性能很好
CSS 和 Javascript 如何影响解析
因为解析进行时是单线程的,所以当解析时遇到了 script
标签,它就会去执行 js 的内容,而如果 js 是以引入方式,执行前会等待下载的完成;因为js执行可能会操纵前面的css
样式,所以如果前面有css需要下载,它会等待css下载解析完成
script属性中async和defer的区别?
不写两个属性时:构建dom执行至script标签时,会阻塞等待script内容下载并执行完毕。
async:异步下载,下载完成后,立即执行script的内容
defer:异步下载,等到dom构建完成后,再执行script的内容
下面一张图说明了这三者区别
所以它们的区别在于加载完成后的时机
注意如果有多个含async的script标签,script执行的先后顺序是无法预知的
内存泄漏
内存泄漏是什么?
内存泄漏简单理解:无用的内存还在占用,得不到释放和归还。比较严重时,无用的内存会持续递增,从而导致整个系统卡顿,甚至崩溃。
如何防止内存泄漏?
防止意外的全局变量
// 在全局作用域下定义 function count(number) { // basicCount 相当于 window.basicCount = 2; basicCount = 2; return basicCount + number; }
被遗忘的计时器 / 事件监听器
比如 vue中的
setInterval
/addEventListener
,如果没有在组件销毁前clear它就会一直存在内存中执行/监听 ,且涉及的内存都不会回收闭包:非必要时不要使用闭包,首先是它占用内存相对更多,第二,它在未使用时,自由变量一直保存,无法被回收
没有脱离 js 中引用的Dom元素
var button = document.querySelector('#button'), document.body.removeChild(button) // button = null
上面的例子 button 元素 虽然在页面上移除了,但是内存指向换为了
button
,内存占用还是存在的。所以上面的代码还需要这样写:button = null
,手动释放这个内存。
如何监控和排查内存泄漏?
在浏览器调试台 performce 选项,开启监控 memory,梯状上升就是内存泄漏了
分析快照,找到内存泄漏点
垃圾回收
栈回收
一个函数执行结束后,ESP 往下移就代表回收了这个 执行上下文,因为有新的执行上下文会直接覆盖
堆回收
代际假说
- 大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问
- 不死的对象,会活得很久,例如:window、document、window下的全局对象
所以假说把堆分为 新生代 和 老生代 两个区域。新生代存放生存时间短的对象,老生代存放生存时间长的对象。
新生代的容量较小(1-8m),老生代的容量很大。
垃圾回收器又分为 主垃圾回收器 和 副垃圾回收器,主垃圾回收器负责老生代的回收,副垃圾回收器负责新生代的回收
垃圾回收过程
一般垃圾回收分为三个阶段
- 标记。区分出活动对象和非活动对象,非活动对象是可以被垃圾回收的对象。
- 清除。标记后清除内存中被标记为可回收的对象
- 内存整理。一般情况下,频繁回收对象后,内存中会存在大量不连续的空间,这些内存空间被称为内存碎片。垃圾回收器就需要对内存进行整理以保证可以满足分配足够大的连续的内存空间的需求。
副垃圾回收器
主要负责新生区垃圾回收。
新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
采用Scavenge算法
将新生代分为对象区域和空闲区域,新加入的对象会被放入对象区域,当对象区域要满时进行一次垃圾清理操作:首先对垃圾进行标记,标记完成后副垃圾回收器将存活的对象按自然顺序复制入空闲区(这样就没有内存碎片),最终后对象区与空闲区的角色翻转。角色翻转的操作可以使两块区域无限地使用下去
对象晋升策略:经历了两次垃圾回收操作还存活的对象将会被放入老生区
主垃圾回收器
老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。由于老生区的对象比较大,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。
因此主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。
对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存,于是又产生了另外一种算法——标记 - 整理(Mark-Compact),这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
)
标记 - 清除(Mark-Sweep)
首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。比如
function foo(){
var a = 1
var b = {name:"极客邦"}
function showName(){
var c = 2
var d = {name:"极客时间"}
}
showName()
}
foo()
当 showName()函数执行退出之后,这段代码的调用栈和堆空间如下图所示:标记过程从上图你可以大致看到垃圾数据的标记过程,当 showName 函数执行结束之后,ESP 向下移动,指向了 foo 函数的执行上下文,这时候如果遍历调用栈,是不会找到引用 1003 地址的变量,也就意味着 1003 这块数据为垃圾数据,被标记为红色。 由于 1050 这块数据被变量 b 引用了,所以这块数据会被标记为活动对象。这就是大致的标记过程。
增量标记
js 的垃圾回收也是运行在主线程上的,它会暂停 js 的执行,直至一次回收完毕。一次完整的垃圾回收导致的 js 暂停,被称为 全停顿
。
如果堆中的数据比较大,全停顿时间过长,就会导致应用的性能和响应能力下降。
为了降低老生代的垃圾回收而造成的卡顿(新生代的区较小全停顿影响不大),V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法
。
垃圾回收的子任务穿插在 js 的执行过程中,用户就不会因垃圾回收而感到页面卡顿了。
储存
SessionStorage
- 同步,会阻塞主线程的执行。
- 一般用于储存临时性的少量的数据。
- SessionStorage 是标签级别的,跟随者标签的生命周期,并且会随着标签的销毁而清空数据。
- 它只能储存字符串,大小限制大约为 5MB。
LocalStorage
同步,会阻塞主线程的执行。
localStorage
只要在相同的协议、相同的主机名、相同的端口下,就能读取/修改到同一份localStorage数据。由于浏览器的安全策略,localstorage 是无法跨域的,也无法让子域名继承父域名的 localstorage 数据,与 cookie 差别很大。
localStorage
理论上来说是永久有效的,即不主动清空的话就不会消失它只能储存字符串,大小限制大约为 5MB。
IndexedDB
Indexed DB 的操作是异步的,不会阻塞主线程的执行,可以在 window、web workers、service workers 环境中使用。
IndexedDB 是基于文件存储的,API 较为复杂,包含 v1 v2 的差异,建议通过类库来使用,比如:Dexie.js。
工程化
前端模块化
IIFE(immediate invoke function expression)立即执行函数
CommonJS 服务端模块化规范
require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。
module.exports.x = x; module.exports.addX = addX; var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径 console.log(example.x); // 5 console.log(example.addX(1)); // 6
输入返回值同函数,只是值的拷贝
AMD 浏览器规范 异步加载模块
CMD 浏览器规范 结合了AMD和CommonJS,实现者是Sea.js
ES6模块化
- ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。 CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。
CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
webpack
webpack 是什么?
webpack
是个静态模块打包工具。 在 webpack
看来,项目里所有资源皆模块,利用资源依赖关系,把各模块之间关联起来。 简单讲就: webpack
对有依赖关系的多个模块文件进行打包处理后,生成浏览器可以直接 高效运行的资源。 通过 入口文件
开始,利用 递归
找到直接依赖或间接依赖的所有模块,并在内部构建一个能映射出项目所需的所有模块的 依赖图
,并进行 webpack
打包生成一个或多个 bundle
文件。
loader和plugin
loader:模块转换器,webpack
将一切文件视为模块,但 webpack
只能解析 JavaScript
文件,而 loader 作用是让 webpack
拥有了加载 和 解析非 JavaScript
文件的能力。
plugin:在 webpack
构建流程中的特定时机注入扩展逻辑,让它具有更多的灵活性。在 webpack
运行的生命周期中会广播出许多事件,plugin
可以监听这些事件,在合适的时机通过 webpack
提供的 API 改变输出结果。
bundle,chunk,module 是什么?
bundle
:是由 webpack
打包出来的文件, chunk
:代码块,一个 chunk
由多个模块组合而成,用于代码的合并和分割。 module
:是开发中的单个模块,在 webpack
的世界,一切皆模块,一个模块对应一个文件,webpack
会从配置的 entry
中递归开始找出所有依赖的模块。
常用loader
- styler-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。
- css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
- sass-loader
- postcss-loader 补全 css 兼容前缀
- babel-loader:将ES6转为ES5
- vue-loader
- source-map-loader:加载额外的Source-map文件,方便断点调试
- ts-loader
常用plugin
clean-webpack-plugin
: 删除打包文件html-webpack-plugin
:简化 HTML 文件创建 (依赖于 html-loader)CompressionWebpackPlugin
:打包时开启gzip压缩SplitChunksPlugin
:提取公共代码,分割代码HotModuleReplacementPlugin
:热替换模块happypack
:实现多线程加速编译
webpack 的热更新原理
热更新又称热替换(Hot Module Replacement),缩写为 HMR
,基于 webpack-dev-server
。
浏览器的网页通过websocket协议与服务器建立起一个长连接,并监听本地文件的改动;
当服务器的css/js/html进行了修改的时候,服务器会向前端发送一个更新的消息,如果是css或者html发生了改变,网页执行js直接操作dom,局部刷新,如果是js发生了改变,只能刷新整个页面。
webpack 优化性能
用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。
gzip压缩
compression-webpack-plugin
new CompressionWebpackPlugin({ filename: '[path].gz[query]', algorithm: 'gzip', test: new RegExp( '\\.(' + productionGzipExtensions.join('|') + ')$' ), threshold: 10240, // 只有大小大于该值的资源会被处理 10240 minRatio: 0.8, // 只有压缩率小于这个值的资源才会被处理 deleteOriginalAssets: false // 删除原文件 })
路由懒加载
import(/*webpackChunkName: 'xx'* 'yourpath'/)
动态引入语法const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
分割代码、提取公共模块
SplitChunksPlugin
config.optimization.splitChunks({ chunks: 'all', cacheGroups: { libs: { name: 'chunk-libs', test: /[\\/]node_modules[\\/]/, priority: 10, chunks: 'initial' // only package third parties that are initially dependent }, elementUI: { name: 'chunk-elementUI', // split elementUI into a single package priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm }, commons: { name: 'chunk-commons', test: resolve('src/components'), // can customize your rules minChunks: 3, // minimum common number priority: 5, reuseExistingChunk: true } } })
压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css
利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径
删除无用的代码(Tree Shaking)。将代码中没有用到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimizer来实现
Tree shake
目的是消除 无用的代码
Uglify可以消除 不可能执行的代码(DCE Dead Code Elimination)
依赖于ES6的模块特性,与运行时无关,可以静态分析
webpack 的构建流程
webpack
的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件 和
Shell
语句中读取与合并参数,得出最终的参数; - 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的
run
方法开始执行编译; - 确定入口:根据配置中的
entry
找出所有的入口文件; - 编译模块:从入口文件出发,调用所有配置的
Loader
对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理; 完成模块编译:在经过第4步使用Loader
翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系; - 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的
Chunk
,再把每个Chunk
转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会; - 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 在以上过程中,
webpack
会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用webpack
提供的API
改变webpack
的运行结果。
性能优化
两种性能指标
- 感知性能:站在用户视角的主观的可感知的性能。
- 客观性能:站在开发者视角的可客观度量的性能。
优化类型
构建优化
上文 "webpack 如何优化性能"
网络优化
请求缓存
设置 control-cache:max-age 设置强缓存(200 状态码)
设置 e-tag,if-modified-since 等设置协商缓存(304 状态码)
开启 http2
CDN 加速静态资源和公共代码的获取
其他
懒加载、预加载、节流函数
感知优化
转场动画
骨架屏
减少白屏时间
- 加速或减少HTTP请求损耗:使用CDN加载公用库,使用强缓存和协商缓存,使用域名收敛,小图片使用Base64代替,使用Get请求代替Post请求,设置 Access-Control-Max-Age 减少预检请求,页面内跳转其他域名或请求其他域名的资源时使用浏览器prefetch预解析等;
- 延迟加载:非重要的库、非首屏图片延迟加载,SPA的组件懒加载等;
- 减少请求内容的体积:开启服务器Gzip压缩,JS、CSS文件压缩合并,减少cookies大小,SSR直接输出渲染后的HTML等;
- 浏览器渲染原理:优化关键渲染路径,尽可能减少阻塞渲染的JS、CSS;
- 优化用户等待体验:白屏使用加载进度条、loading图、骨架屏代替等;
- 服务端渲染 SSR
懒加载的实现方式
图片的初始 src 设置为一张加载图,在自定义属性 data-src 上设置真正图片的地址,监听视口的改变,当图片进入视口或将进入视口时,将 src 设置为真正的地址
监听的方式主要是两种
onscroll
滚动的回调需要计算图片到视口的距离。因为事件触发频繁且需要每张图片都计算,所以应该设置节流函数
IntersectionObserver 用于监测 dom 进入视口和离开视口
本文转自 https://juejin.cn/post/6942799422954668069,如有侵权,请联系删除。