防抖
防抖(debounce
):触发高频事件 N 秒后只会执行一次,如果 N 秒内事件再次触发,则会重新计时。类似王者荣耀的回城功能,你反复触发回城功能,那么只认最后一次,从最后一次触发开始计时。
核心思想:每次事件触发就清除原来的定时器,建立新的定时器。使用apply或call调用传入的函数。函数内部支持使用 this 和 event 对象;
应用:防抖常应用于用户进行搜索输入节约请求资源,window
触发resize
事件时进行防抖只触发一次。
实现:
代码语言:javascript复制function debounce(fn, delay) {
// 利用闭包的原理
let timer = null;
return function(...args){
if(timer) clearTimeout(timer);
timer = setTimeout(() => {
// 改变 this 指向为调用 debounce 所指的对象
fn.call(this, ...args);
// fn.apply(this, args);
}, delay);
}
}
并发与并行的区别?
- 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
- 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。
前端进阶面试题详细解答
meta 标签:自动刷新/跳转
假设要实现一个类似 PPT 自动播放的效果,你很可能会想到使用 JavaScript 定时器控制页面跳转来实现。但其实有更加简洁的实现方法,比如通过 meta 标签来实现:
代码语言:html复制<meta http-equiv="Refresh" content="5; URL=page2.html">
上面的代码会在 5s 之后自动跳转到同域下的 page2.html 页面。我们要实现 PPT 自动播放的功能,只需要在每个页面的 meta 标签内设置好下一个页面的地址即可。
另一种场景,比如每隔一分钟就需要刷新页面的大屏幕监控,也可以通过 meta 标签来实现,只需去掉后面的 URL 即可:
代码语言:html复制<meta http-equiv="Refresh" content="60">
meta viewport相关
代码语言:html复制<!DOCTYPE html> <!--H5标准声明,使用 HTML5 doctype,不区分大小写-->
<head lang=”en”> <!--标准的 lang 属性写法-->
<meta charset=’utf-8′> <!--声明文档使用的字符编码-->
<meta http-equiv=”X-UA-Compatible” content=”IE=edge,chrome=1″/> <!--优先使用 IE 最新版本和 Chrome-->
<meta name=”description” content=”不超过150个字符”/> <!--页面描述-->
<meta name=”keywords” content=””/> <!-- 页面关键词-->
<meta name=”author” content=”name, email@gmail.com”/> <!--网页作者-->
<meta name=”robots” content=”index,follow”/> <!--搜索引擎抓取-->
<meta name=”viewport” content=”initial-scale=1, maximum-scale=3, minimum-scale=1, user-scalable=no”> <!--为移动设备添加 viewport-->
<meta name=”apple-mobile-web-app-title” content=”标题”> <!--iOS 设备 begin-->
<meta name=”apple-mobile-web-app-capable” content=”yes”/> <!--添加到主屏后的标题(iOS 6 新增)
是否启用 WebApp 全屏模式,删除苹果默认的工具栏和菜单栏-->
<meta name=”apple-itunes-app” content=”app-id=myAppStoreID, affiliate-data=myAffiliateData, app-argument=myURL”>
<!--添加智能 App 广告条 Smart App Banner(iOS 6 Safari)-->
<meta name=”apple-mobile-web-app-status-bar-style” content=”black”/>
<meta name=”format-detection” content=”telphone=no, email=no”/> <!--设置苹果工具栏颜色-->
<meta name=”renderer” content=”webkit”> <!-- 启用360浏览器的极速模式(webkit)-->
<meta http-equiv=”X-UA-Compatible” content=”IE=edge”> <!--避免IE使用兼容模式-->
<meta http-equiv=”Cache-Control” content=”no-siteapp” /> <!--不让百度转码-->
<meta name=”HandheldFriendly” content=”true”> <!--针对手持设备优化,主要是针对一些老的不识别viewport的浏览器,比如黑莓-->
<meta name=”MobileOptimized” content=”320″> <!--微软的老式浏览器-->
<meta name=”screen-orientation” content=”portrait”> <!--uc强制竖屏-->
<meta name=”x5-orientation” content=”portrait”> <!--QQ强制竖屏-->
<meta name=”full-screen” content=”yes”> <!--UC强制全屏-->
<meta name=”x5-fullscreen” content=”true”> <!--QQ强制全屏-->
<meta name=”browsermode” content=”application”> <!--UC应用模式-->
<meta name=”x5-page-mode” content=”app”> <!-- QQ应用模式-->
<meta name=”msapplication-tap-highlight” content=”no”> <!--windows phone 点击无高亮
设置页面不缓存-->
<meta http-equiv=”pragma” content=”no-cache”>
<meta http-equiv=”cache-control” content=”no-cache”>
<meta http-equiv=”expires” content=”0″>
对JSON的理解
JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。
在项目开发中,使用 JSON 作为前后端数据交换的方式。在前端通过将一个符合 JSON 格式的数据结构序列化为
JSON 字符串,然后将它传递到后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,以此来实现前后端数据的一个传递。
因为 JSON 的语法是基于 js 的,因此很容易将 JSON 和 js 中的对象弄混,但是应该注意的是 JSON 和 js 中的对象不是一回事,JSON 中对象格式更加严格,比如说在 JSON 中属性值不能为函数,不能出现 NaN 这样的属性值等,因此大多数的 js 对象是不符合 JSON 对象的格式的。
在 js 中提供了两个函数来实现 js 数据结构和 JSON 格式的转换处理,
- JSON.stringify 函数,通过传入一个符合 JSON 格式的数据结构,将其转换为一个 JSON 字符串。如果传入的数据结构不符合 JSON 格式,那么在序列化的时候会对这些值进行对应的特殊处理,使其符合规范。在前端向后端发送数据时,可以调用这个函数将数据对象转化为 JSON 格式的字符串。
- JSON.parse() 函数,这个函数用来将 JSON 格式的字符串转换为一个 js 数据结构,如果传入的字符串不是标准的 JSON 格式的字符串的话,将会抛出错误。当从后端接收到 JSON 格式的字符串时,可以通过这个方法来将其解析为一个 js 数据结构,以此来进行数据的访问。
左右居中方案
- 行内元素:
text-align: center
- 定宽块状元素: 左右
margin
值为auto
- 不定宽块状元素:
table
布局,position transform
/* 方案1 */
.wrap {
text-align: center
}
.center {
display: inline;
/* or */
/* display: inline-block; */
}
/* 方案2 */
.center {
width: 100px;
margin: 0 auto;
}
/* 方案2 */
.wrap {
position: relative;
}
.center {
position: absulote;
left: 50%;
transform: translateX(-50%);
}
对BFC的理解,如何创建BFC
先来看两个相关的概念:
- Box: Box 是 CSS 布局的对象和基本单位,⼀个⻚⾯是由很多个 Box 组成的,这个Box就是我们所说的盒模型。
- Formatting context:块级上下⽂格式化,它是⻚⾯中的⼀块渲染区域,并且有⼀套渲染规则,它决定了其⼦元素将如何定位,以及和其他元素的关系和相互作⽤。
块格式化上下文(Block Formatting Context,BFC)是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。
通俗来讲:BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。
创建BFC的条件:
- 根元素:body;
- 元素设置浮动:float 除 none 以外的值;
- 元素设置绝对定位:position (absolute、fixed);
- display 值为:inline-block、table-cell、table-caption、flex等;
- overflow 值为:hidden、auto、scroll;
BFC的特点:
- 垂直方向上,自上而下排列,和文档流的排列方式一致。
- 在BFC中上下相邻的两个容器的margin会重叠
- 计算BFC的高度时,需要计算浮动元素的高度
- BFC区域不会与浮动的容器发生重叠
- BFC是独立的容器,容器内部元素不会影响外部元素
- 每个元素的左margin值和容器的左border相接触
BFC的作用:
- 解决margin的重叠问题:由于BFC是一个独立的区域,内部的元素和外部的元素互不影响,将两个元素变为两个BFC,就解决了margin重叠的问题。
- 解决高度塌陷的问题:在对子元素设置浮动后,父元素会发生高度塌陷,也就是父元素的高度变为0。解决这个问题,只需要把父元素变成一个BFC。常用的办法是给父元素设置
overflow:hidden
。 - 创建自适应两栏布局:可以用来创建自适应两栏布局:左边的宽度固定,右边的宽度自适应。
.left{
width: 100px;
height: 200px;
background: red;
float: left;
}
.right{
height: 300px;
background: blue;
overflow: hidden;
}
<div class="left"></div>
<div class="right"></div>
左侧设置float:left
,右侧设置overflow: hidden
。这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠,实现了自适应两栏布局。
性能优化
性能优化是前端开发中避不开的问题,
性能问题无外乎两方面原因:渲染速度慢、请求时间长
。性能优化虽然涉及很多复杂的原因和解决方案,但其实只要通过合理地使用标签,就可以在一定程度上提升渲染速度以及减少请求时间
1. script 标签:调整加载顺序提升渲染速度
- 由于浏览器的底层运行机制,
渲染引擎在解析 HTML 时,若遇到 script 标签引用文件,则会暂停解析过程
,同时通知网络线程加载文件,文件加载后会切换至 JavaScript 引擎来执行对应代码
,代码执行完成之后切换至渲染引擎继续渲染页面
。 - 在这一过程中可以看到,
页面渲染过程中包含了请求文件以及执行文件的时间
,但页面的首次渲染可能并不依赖这些文件,这些请求和执行文件的动作反而延长了用户看到页面的时间,从而降低了用户体验。
为了减少这些时间损耗,可以借助 script 标签的 3 个属性来实现。
async 属性
。立即请求文件,但不阻塞渲染引擎,而是文件加载完毕后阻塞渲染引擎并立即执行文件内容
defer 属性
。立即请求文件,但不阻塞渲染引擎,等到解析完 HTML 之后再执行
文件内容- HTML5 标准 type 属性,对应值为“module”。让浏览器按照 ECMA Script 6 标准将文件当作模块进行解析,默认阻塞效果同 defer,也可以配合 async 在请求完成后立即执行。
绿色的线表示执行解析 HTML ,蓝色的线表示请求文件,红色的线表示执行文件
当渲染引擎解析 HTML 遇到 script 标签引入文件时,会立即进行一次渲染
。所以这也就是为什么构建工具会把编译好的引用 JavaScript 代码的 script 标签放入到 body 标签底部,因为当渲染引擎执行到 body 底部时会先将已解析的内容渲染出来,然后再去请求相应的 JavaScript 文件
2. link 标签:通过预处理提升渲染速度
在我们对大型单页应用进行性能优化时,也许会用到按需懒加载的方式,来加载对应的模块,但如果能合理利用 link
标签的 rel
属性值来进行预加载,就能进一步提升渲染速度。
dns-prefetch
。当link
标签的rel
属性值为“dns-prefetch”时,浏览器会对某个域名预先进行 DNS 解析并缓存
。这样,当浏览器在请求同域名资源的时候,能省去从域名查询 IP 的过程,从而减少时间损耗
。下图是淘宝网设置的 DNS 预解析preconnect
。让浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括DNS 解析、TLS 协商、TCP 握手
,通过消除往返延迟来为用户节省时间prefetch/preload
。两个值都是让浏览器预先下载并缓存某个资源
,但不同的是,prefetch 可能会在浏览器忙时被忽略
,而preload 则是一定会被预先下载
。prerender
。浏览器不仅会加载资源,还会解析执行页面,进行预渲染
这几个属性值恰好反映了浏览器获取资源文件的过程,在这里我绘制了一个流程简图,方便你记忆。
3. 搜索优化
- meta 标签:提取关键信息
- 通过 meta 标签可以设置页面的描述信息,从而让搜索引擎更好地展示搜索结果。
- 示例
<meta name="description" content="全球最大的中文搜索引擎、致力于让网民更便捷地获取信息,找到所求。百度超过千亿的中文网页数据库,可以瞬间找到相关的搜索结果。">
absolute与fixed共同点与不同点
共同点:
- 改变行内元素的呈现方式,将display置为inline-block
- 使元素脱离普通文档流,不再占据文档物理空间
- 覆盖非定位文档元素
不同点:
- abuselute与fixed的根元素不同,abuselute的根元素可以设置,fixed根元素是浏览器。
- 在有滚动条的页面中,absolute会跟着父元素进行移动,fixed固定在页面的具体位置。
Vue 响应式原理
Vue 的响应式原理是核心是通过 ES5 的保护对象的
Object.defindeProperty
中的访问器属性中的 get 和 set 方法,data 中声明的属性都被添加了访问器属性,当读取 data 中的数据时自动调用 get 方法,当修改 data 中的数据时,自动调用 set 方法,检测到数据的变化,会通知观察者 Wacher,观察者 Wacher自动触发重新render 当前组件(子组件不会重新渲染),生成新的虚拟 DOM 树,Vue 框架会遍历并对比新虚拟 DOM 树和旧虚拟 DOM 树中每个节点的差别,并记录下来,最后,加载操作,将所有记录的不同点,局部修改到真实 DOM树上。
- 虚拟DOM (Virtaul DOM): 用 js 对象模拟的,保存当前视图内所有 DOM 节点对象基本描述属性和节点间关系的树结构。用 js 对象,描述每个节点,及其父子关系,形成虚拟 DOM 对象树结构。
- 因为只要在
data
中声明的基本数据类型的数据,基本不存在数据不响应问题,所以重点介绍数组和对象在vue
中的数据响应问题,vue可以检测对象属性的修改,但无法监听数组的所有变动及对象的新增和删除,只能使用数组变异方法及$set
方法。
可以看到,
arrayMethods
首先继承了Array
,然后对数组中所有能改变数组自身的方法,如push
、pop
等这些方法进行重写。重写后的方法会先执行它们本身原有的逻辑,并对能增加数组长度的 3 个方法push
、unshift
、splice
方法做了判断,获取到插入的值,然后把新添加的值变成一个响应式对象,并且再调用ob.dep.notify()
手动触发依赖通知,这就很好地解释了用vm.items.splice
(newLength
) 方法可以检测到变化总结:Vue 采用数据劫持结合发布—订阅模式的方法,通过
Object.defineProperty()
来劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
Observer
遍历数据对象,给所有属性加上setter
和getter
,监听数据的变化compile
解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
Watcher
订阅者是Observer
和Compile
之间通信的桥梁,主要做的事情
- 在自身实例化时往属性订阅器 (
dep
) 里面添加自己 - 待属性变动
dep.notice()
通知时,调用自身的update()
方法,并触发Compile
中绑定的回调
Object.defineProperty() ,那么它的用法是什么,以及优缺点是什么呢?
- 可以检测对象中数据发生的修改
- 对于复杂的对象,层级很深的话,是不友好的,需要经行深度监听,这样子就需要递归到底,这也是它的缺点。
- 对于一个对象中,如果你新增加属性,删除属性,Object.defineProperty()是不能观测到的,那么应该如何解决呢?可以通过
Vue.set()
和Vue.delete()
来实现。
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello'
}
// 模拟 Vue 的实例
let vm = {}
// 数据劫持:当访问或者设置 vm 中的成员的时候,做一些干预操作
Object.defineProperty(vm, 'msg', {
// 可枚举(可遍历)
enumerable: true,
// 可配置(可以使用 delete 删除,可以通过 defineProperty 重新定义)
configurable: true,
// 当获取值的时候执行
get () {
console.log('get: ', data.msg)
return data.msg
},
// 当设置值的时候执行
set (newValue) {
console.log('set: ', newValue)
if (newValue === data.msg) {
return
}
data.msg = newValue
// 数据更改,更新 DOM 的值
document.querySelector('#app').textContent = data.msg
}
})
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
Vue3.x响应式数据原理
Vue3.x
改用Proxy
替代Object.defineProperty
。因为Proxy
可以直接监听对象和数组
的变化,并且有多达13种拦截方法。并且作为新标准将受到浏览器厂商重点持续的性能优化。
Proxy
只会代理对象的第一层,那么Vue3
又是怎样处理这个问题的呢?
判断当前
Reflect.get的
返回值是否为Object
,如果是则再通过reactive
方法做代理, 这样就实现了深度观测。
监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?
代码语言:javascript复制我们可以判断
key
是否为当前被代理对象target
自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger
// 模拟 Vue 中的 data 选项
let data = {
msg: 'hello',
count: 0
}
// 模拟 Vue 实例
let vm = new Proxy(data, {
// 当访问 vm 的成员会执行
get (target, key) {
console.log('get, key: ', key, target[key])
return target[key]
},
// 当设置 vm 的成员会执行
set (target, key, newValue) {
console.log('set, key: ', key, newValue)
if (target[key] === newValue) {
return
}
target[key] = newValue
document.querySelector('#app').textContent = target[key]
}
})
// 测试
vm.msg = 'Hello World'
console.log(vm.msg)
Proxy 相比于 defineProperty 的优势
- 数组变化也能监听到
- 不需要深度遍历监听
代码语言:javascript复制
Proxy
是ES6
中新增的功能,可以用来自定义对象中的操作
let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
// 可以很方便的使用 Proxy 来实现一个数据绑定和监听
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
总结
- Vue
- 记录传入的选项,设置
$data/$el
- 把
data
的成员注入到Vue
实例 - 负责调用
Observer
实现数据响应式处理(数据劫持) - 负责调用
Compiler
编译指令/插值表达式等
- 记录传入的选项,设置
Observer
- 数据劫持
- 负责把
data
中的成员转换成getter/setter
- 负责把多层属性转换成
getter/setter
- 如果给属性赋值为新对象,把新对象的成员设置为
getter/setter
- 负责把
- 添加
Dep
和Watcher
的依赖关系 - 数据变化发送通知
- 数据劫持
Compiler
- 负责编译模板,解析指令/插值表达式
- 负责页面的首次渲染过程
- 当数据变化后重新渲染
Dep
- 收集依赖,添加订阅者(
watcher
) - 通知所有订阅者
- 收集依赖,添加订阅者(
Watcher
- 自身实例化的时候往
dep
对象中添加自己 - 当数据变化
dep
通知所有的Watcher
实例更新视图
- 自身实例化的时候往
内存泄露
- 意外的全局变量: 无法被回收
- 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
- 事件监听: 没有正确销毁 (低版本浏览器可能出现)
- 闭包
- 第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
- 第二种情况是我们设置了setInterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 第三种情况是我们获取一个DOM元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
- 第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。
dom
引用:dom
元素被删除时,内存中的引用未被正确清空- 控制台
console.log
打印的东西
可用
chrome
中的timeline
进行内存标记,可视化查看内存的变化情况,找出异常点。
内存泄露排查方法(opens new window)
缓存机制
1. 首先得明确 http 缓存的好处
- 减少了冗余的数据传输,减少网费
- 减少服务器端的压力
Web
缓存能够减少延迟与网络阻塞,进而减少显示某个资源所用的时间- 加快客户端加载网页的速度
2. 常见 http 缓存的类型
- 私有缓存(一般为本地浏览器缓存)
- 代理缓存
3. 然后谈谈本地缓存
本地缓存是指浏览器请求资源时命中了浏览器本地的缓存资源,浏览器并不会发送真正的请求给服务器了。它的执行过程是
- 第一次浏览器发送请求给服务器时,此时浏览器还没有本地缓存副本,服务器返回资源给浏览器,响应码是
200 OK
,浏览器收到资源后,把资源和对应的响应头一起缓存下来 - 第二次浏览器准备发送请求给服务器时候,浏览器会先检查上一次服务端返回的响应头信息中的
Cache-Control
,它的值是一个相对值,单位为秒,表示资源在客户端缓存的最大有效期,过期时间为第一次请求的时间减去Cache-Control
的值,过期时间跟当前的请求时间比较,如果本地缓存资源没过期,那么命中缓存,不再请求服务器 - 如果没有命中,浏览器就会把请求发送给服务器,进入缓存协商阶段。
与本地缓存相关的头有:
Cache-Control
、Expires
,Cache-Control
有多个可选值代表不同的意义,而Expires
就是一个日期格式的绝对值。
3.1 Cache-Control
Cache-Control
是HTPP
缓存策略中最重要的头,它是HTTP/1.1
中出现的,它由如下几个值
no-cache
:不使用本地缓存。需要使用缓存协商,先与服务器确认返回的响应是否被更改,如果之前的响应中存在ETag
,那么请求的时候会与服务端验证,如果资源未被更改,则可以避免重新下载no-store
:直接禁止游览器缓存数据,每次用户请求该资源,都会向服务器发送一个请求,每次都会下载完整的资源public
:可以被所有的用户缓存,包括终端用户和CDN
等中间代理服务器。private
:只能被终端用户的浏览器缓存,不允许CDN
等中继缓存服务器对其缓存。max-age
:从当前请求开始,允许获取的响应被重用的最长时间(秒)。must-revalidate
,当缓存过期时,需要去服务端校验缓存的有效性。
Cache-Control: public, max-age=1000
代码语言:javascript复制注意,虽然你可能在其他资料中看到可以使用 meta 标签来设置缓存,比如像下面的形式:
<meta http-equiv="expires" content="Wed, 20 Jun 2021 22:33:00 GMT"
但在 HTML5 规范中,并不支持这种方式,所以尽量不要使用 meta 标签来设置缓存
。
3.2 Expires
代码语言:javascript复制
Expires
是HTTP/1.0
出现的头信息,同样是用于决定本地缓存策略的头,它是一个绝对时间,时间格式是如Mon, 10 Jun 2015 21:31:12 GMT
,只要发送请求时间是在Expires
之前,那么本地缓存始终有效,否则就会去服务器发送请求获取新的资源。如果同时出现Cache-Control:max-age
和Expires
,那么max-age
优先级更高。他们可以这样组合使用
Cache-Control: public
Expires: Wed, Jan 10 2018 00:27:04 GMT
3.3 所谓的缓存协商
当第一次请求时服务器返回的响应头中存在以下情况时
- 没有
Cache-Control
和Expires
Cache-Control
和Expires
过期了Cache-Control
的属性设置为no-cache
时
那么浏览器第二次请求时就会与服务器进行协商,询问浏览器中的缓存资源是不是旧版本,需不需要更新,此时,服务器就会做出判断,如果缓存和服务端资源的最新版本是一致的,那么就无需再次下载该资源,服务端直接返回
304 Not Modified
状态码,如果服务器发现浏览器中的缓存已经是旧版本了,那么服务器就会把最新资源的完整内容返回给浏览器,状态码就是200 Ok
,那么服务端是根据什么来判断浏览器的缓存是不是最新的呢?其实是根据HTTP
的另外两组头信息,分别是:Last-Modified/If-Modified-Since
与ETag/If-None-Match
。
Last-Modified 与 If-Modified-Since
具体工作流程如下:
- 浏览器第一次请求资源时,服务器会把资源的最新修改时间
Last-Modified:Thu, 29 Dec 2011 18:23:55 GMT
放在响应头中返回给浏览器 - 第二次请求时,浏览器就会把上一次服务器返回的修改时间放在请求头
If-Modified-Since:Thu, 29 Dec 2011 18:23:55
发送给服务器,服务器就会拿这个时间跟服务器上的资源的最新修改时间进行对比 - 服务端再次收到请求,根据请求头
If-Modified-Since
的值,判断相关资源是否有变化,如果没有,则返回304 Not Modified
,并且不返回资源内容,浏览器使用资源缓存值;否则正常返回资源内容,且更新Last-Modified
响应头内容。
如果两者相等或者大于服务器上的最新修改时间,那么表示浏览器的缓存是有效的,此时缓存会命中,服务器就不再返回内容给浏览器了,同时
Last-Modified
头也不会返回,因为资源没被修改,返回了也没什么意义。如果没命中缓存则最新修改的资源连同Last-Modified
头一起返回
这种方式虽然能判断缓存是否失效,但也存在两个问题:
- 精度问题 ,
Last-Modified
的时间精度为秒,如果在1
秒内发生修改,那么缓存判断可能会失效; - 准度问题 ,考虑这样一种情况,如果一个文件被修改,然后又被还原,内容并没有发生变化,在这种情况下,浏览器的缓存还可以继续使用,但因为修改时间发生变化,也会重新返回重复的内容。
Expires: Fri, Jan 12 2018 00:27:04 GMT
Last-Modified: Wed, Jan 10 2018 00:27:04 GMT
代码语言:shell复制
这组头信息是基于资源的修改时间来判断资源有没有更新,另一种方式就是根据资源的内容来判断,就是接下来要讨论的
ETag
与If-None-Match
ETag与If-None-Match
为了解决精度问题和准度问题
,HTTP 提供了另一种不依赖于修改时间,而依赖于文件哈希值的精确判断缓存的方式,那就是响应头部字段 ETag 和请求头部字段 If-None-Match。
ETag/If-None-Match
与Last-Modified/If-Modified-Since
的流程其实是类似的,唯一的区别是它基于资源的内容的摘要信息(比如MD5 hash
)来判断浏览器发送第二次请求时,会把第一次的响应头信息
ETag
的值放在If-None-Match
的请求头中发送到服务器,与最新的资源的摘要信息对比,如果相等,取浏览器缓存,否则内容有更新,最新的资源连同最新的摘要信息返回。用ETag
的好处是如果因为某种原因到时资源的修改时间没改变,那么用ETag
就能区分资源是不是有被更新。
具体工作流程如下:
- 浏览器第一次请求资源,服务端在返响应头中加入
Etag
字段,Etag
字段值为该资源的哈希值 - 当浏览器再次跟服务端请求这个资源时,在请求头上加上
If-None-Match
,值为之前响应头部字段ETag
的值; - 服务端再次收到请求,将请求头
If-None-Match
字段的值和响应资源的哈希值进行比对,如果两个值相同,则说明资源没有变化,返回304 Not Modified
;否则就正常返回资源内容,无论是否发生变化,都会将计算出的哈希值放入响应头部的ETag
字段中
这种缓存比较的方式也会存在一些问题,具体表现在以下两个方面。
- 计算成本 。生成哈希值相对于读取文件修改时间而言是一个开销比较大的操作,尤其是对于大文件而言。如果要精确计算则需读取完整的文件内容,如果从性能方面考虑,只读取文件部分内容,又容易判断出错。
- 计算误差 。HTTP 并没有规定哈希值的计算方法,所以不同服务端可能会采用不同的哈希值计算方式。这样带来的问题是,同一个资源,在两台服务端产生的 Etag 可能是不相同的,所以对于使用服务器集群来处理请求的网站来说,使用 Etag 的缓存命中率会有所降低。
代码语言:shell复制需要注意的是,
强制缓存的优先级高于协商缓存
,在协商缓存中,Etag 优先级比 Last-Modified
高
Cache-Control: public, max-age=31536000
ETag: "15f0fff99ed5aae4edffdd6496d7131f"
代码语言:shell复制If-None-Match: "15f0fff99ed5aae4edffdd6496d7131f"
缓存位置
浏览器缓存的位置的话,可以分为四种,优先级从高到低排列分别