JavaScript
事件循环
- JS是单线程语言,所有任务需要排队依次执行
- JS任务分同步任务和异步任务
- 异步任务又分为宏任务和微任务
- 宏任务包括:
- script
- setTimeout
- setInterval
- requestAnimationFrame
- requestIdleCallback
- UI rendering
- MessageChannel
- 微任务包括:
- Promise
- MutationObserver
- queueMicrotask
- process.nextTick()
- 主线程首先执行主执行栈代码,在执行的过程中产生的异步任务分别放入宏任务队列或者微任务队列队尾
- 等待执行栈为空时,依次从微任务队列头部取出微任务放入执行栈执行,直到清空微任务队列
- 接着执行 UI 渲染,完成一次事件循环
- 取出宏任务队列头部的任务放入执行栈执行,重复以上操作
JS 的垃圾回收机制和内存泄漏
垃圾回收机制
JS 引擎在创建变量(对象,字符串等)时自动进分配内存,并且在不使用它们时自动释放,释放的过程称为垃圾回收。JS 引擎常用的垃圾回收策略有:
- 标记清理: 标记内存中存储的所有变量(标记方法有很多种)。然后,会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉。在此之后再被加上标记的变量就是待删除的。随后垃圾回收程序做一次内存清理,销毁带标记的所有值并收回它们的内存。
- 引用计数: 对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为 1。如果同一个值又被赋给另一个变量,那么引用数加 1。类似地,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减 1。当一个值的引用数为 0 时,就说明没办法再访问到这个值了,因此可以安全地收回其内存。垃圾回收程序下次运行的时候就会释放引用数为 0 的值的内存。
内存泄漏
如果某些内存区域没有得到及时的释放,就会导致内存泄漏。常见的内存泄漏有:
- 没有使用任何关键字声明变量,默认会将此变量作为 window 的属性
- 被遗忘的定时器,并且定时器里引用了外部变量
- 使用不当的闭包
- 未清理的 DOM 引用
- 对象的循环引用
- 使用 Set 和 Map 存储复杂类型的数据,可以用 WeakSet 和 WeakMap 代替
Promise
对 Promise 的理解
- 解决异步操作回调函数嵌套问题,是 ES6 新增的接口对象
- Promise有三种状态:pending (执行中)、fulfilled (已成功)、rejected (已失败)
- 状态只能从 pengding 变为 fulfilled 或 rejected,并且状态不可逆,一旦改变就不会再改变
- async、await 是 Promise 的语法糖,是为了优化 then 链而开发的,使用try catch语句捕获 await 异常
Promise的 all()、race()、allSettled() 方法的作用
- all() 方法接收一组可迭代的 promise,当所有 promise 都变为 fulfilled 状态时,才返回成功,返回的成功数据是一个数组,包含每个 promise 的 resolve 返回值,当有一个 promise 变为 rejected 状态时,all 方法立即返回失败,失败数据是对应 promise 的 reject 返回值
- race() 方法接收一组可迭代的 promise,一旦有 promise 成功或拒绝,race 方法就会成功或拒绝
- allSettled() 方法接收一组可迭代的 promise,当所有 promise 成功或拒绝时,变为 fulfilled 状态,返回描述每个promise 结果的对象数组
实现 Promise.all()
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
const result = []
const len = promises.length
let count = 0
for (let [i, p] of promises.entries()) {
p.then((res) => {
count++
result[i] = res
if (count === len) {
return resolve(result)
}
}).catch((error) => {
return reject(error)
})
}
})
}
判断 Promise 对象
const isPromise = (val) => {
return typeof val !== null && val === 'object' && typeof val.then === 'function' && typeof val.catch === 'function'
}
浅拷贝与深拷贝
- 深拷贝是指对象的属性与拷贝的源对象属性之间不共享引用的副本,对属性的修改不会影响到另一个对象
- 浅拷贝是指对象的属性与拷贝的源对象属性之间共享相同引用的副本,修改对象属性会导致其他对象也发生更改
- JS 所有内置的复制操作创建的是浅拷贝:
- 展开运算符(...)
- Array.prototype.concat()
- Array.prototype.slice()
- Array.from()
- Object.assign()
- Object.create()
JSON.parse、JSON.stringify 实现深拷贝的缺陷
- 会忽略 undefined、symbol、function
- Map, Set, RegExp 会被转换为空对象
- Date 会被转换为描述日期时间字符串
- 不支持循环引用对象的拷贝,导致栈溢出
如何解决深拷贝的循环引用问题
需要一个变量容器,将已拷贝的对象放置到容器变量中,优先从容器变量中获取目标值。
function deepClone(target) {
const map = new WeakMap()
function clone (target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {}
if (map.has(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
for (const key in target) {
cloneTarget[key] = clone(target[key])
}
return cloneTarget
} else {
return target
}
}
return clone(target)
}
函数柯里化
函数柯里化是指将接受多个参数的函数转换为接受一个参数的函数,并返回接受其余参数而且返回结果的函数。 函数柯里化的作用有:
- 参数复用,当在多次调用同一个函数,并且传递的参数绝大多数是相同的,那么该函数是一个很好的柯里化候选
- 提前返回,多次调用多次内部判断,可以直接把第一次判断的结果返回外部接收。例如立即执行函数提前判断环境决定返回哪一个函数
- 延迟执行,避免重复的去执行程序,等真正需要结果的时候再执行。例如 Function.prototype.bind()
防抖和节流
防抖是在连续触发事件的情况下,仅执行最后一次的回调函数。在指定时间间隔内,如果连续触发事件,会重新开始计时。应用场景:
- input搜索、输入联想、浏览器窗口改变大小、窗口滚动。
节流是在连续触发事件的情况下,在指定时间间隔内仅执行一次回调函数。在指定时间间隔内,如果再触发事件,不再执行回调函数。应用场景:
- 表单提交、按钮点击、上拉加载。
// 防抖
const debounce = (fn, delay = 1000) => {
let timer = null
return (...args) => {
if (timer) {
clearTimeout(timer)
timer = null
}
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
// 节流 时间戳实现 首次会触发
const throttle = (fn, delay = 1000) => {
let timer = 0
return (...args) => {
const now = Date.now()
if (now - timer >= delay) {
timer = now;
fn.apply(this, args)
}
}
}
// 节流 定时器实现 首次不会触发
const throttle1 = (fn, delay = 1000) => {
let timer = null
return (...args) => {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay)
}
}
}
实现 call、apply、bind
call、apply、bind 都是 Function 原型对象里的方法,作用是改变函数执行时的 this 上下文。区别是:
- apply 的第二个参数是数组,函数参数以数组方式传入。
- bind 返回修改 this 后的函数,不会立即执行,需要手动调用执行。
Function.prototype._call = function(ctx, ...args) {
const context = Object(ctx)
const key = Symbol()
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
Function.prototype._apply = function(ctx, args) {
const context = Object(ctx)
const key = Symbol()
context[key] = this
const result = context[key](...args)
delete context[key]
return result
}
Function.prototype._bind = function(ctx, ...args) {
const self = this
return function(...rest) {
self._call(ctx, ...args, ...rest)
}
}
实现 instanceof
instanceof 是指左侧对象的原型链上是否存在右侧构造函数的原型,存在即返回 true。由于原型链和 prototype 都可以修改,所以有时 instanceof 判断无法达到预期效果。
function isInstanceof(obj, fn) {
if (obj === null || obj === undefined || fn === null || fn === undefined) {
return false
}
if (obj.__proto__ === fn.prototype) {
return true
}
return isInstanceof(obj.__proto__, fn)
}
观察者模式和发布订阅模式的区别
- 观察者模式定义了一对多的关系,多个观察者对象监听一个目标对象,目标对象的状态变化时,会通知所有的观察者对象更新自己。观察者对象和目标对象之间建立抽象耦合关系。目标对象维护一个观察者列表,观察者对象都有一个共同的接口被目标对象调用。
- 发布订阅模式通过自定义事件订阅主题,统一由调度中心进行处理,实现了发布者与订阅者的解耦,避免了一个对象显示地调用另外一个对象的某个接口。
实现一个带并发限制的异步调度器
class Scheduler {
constructor() {
this._max = 2;
this.unwork = [];
this.working = [];
}
add(asyncTask) {
return new Promise((resolve) => {
asyncTask.resolve = resolve;
if(this.working.length < this._max) {
this.runTask(asyncTask);
} else {
this.unwork.push(asyncTask);
}
})
}
runTask(asyncTask) {
this.working.push(asyncTask);
asyncTask().then(() => {
asyncTask.resolve(); //asyncTask异步任务完成以后,再调用外层Promise的resolve以便add().then()的执行
var index = this.working.indexOf(asyncTask);
this.working.splice(index,1); //从正在进行的任务队列中删除
if(this.unwork.length > 0) {
this.runTask(this.unwork.shift());
}
})
}
}
const timeout = (time) => new Promise(resolve => {
setTimeout(resolve,time);
})
const scheduler = new Scheduler();
const addTask = (time,order) => {
scheduler.add(() => timeout(time)).then(() => console.log(order));
}
addTask(4000,4);
addTask(2000,2);
addTask(3000,3);
addTask(900,1);
移动端适配
移动端适配的目的:在不同尺寸的机型屏幕上 UI 界面表现一致。 首先添加 meta 标签,将页面的宽度设置为屏幕视口宽度。
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
rem 适配
rem 是 css3 的一个相对单位,它根据 html 根元素的 fone-size 来计算最终的 css 属性值。可以根据不同机型屏幕宽度动态调整 html 根元素的 font-size,以达到适配效果。 例如,以屏幕宽度 750px 为基准适配,最大适配到宽度为 1500px 的屏幕:
(function(win, doc) {
function setRem() {
const width = doc.documentElement;
rootHtml.style.fontSize = Math.min(width / 750, 2) * 100 + 'px';
}
setRem();
win.addEventListener('resize', setRem, false);
})(window, document);
也可以使用 postcss-pxtorem 工具进行适配。
vw 适配
vw 适配将屏幕宽度分成 100 等份,1vw 等于 1% 的屏幕宽度。 与 rem 适配原理相同,rem 适配是在 vw 前的过渡方案。 也可以使用 postcss-px-to-viewport 工具进行适配。
其他适配方案
其他适配方案还有:百分比适配、媒体查询。
TypeScript
TypeScript 的优势
- 增加了静态类型,可以在编译阶段检测错误,使代码更健壮
- 类型文件可以在一定程度上充当文档
- 支持 IDE 自动补全
interface 和 type 的区别
- type 类型别名用于给一个类型起个新名字,可以用来声明基本类型、对象类型、函数类型、联合类型、元组和交叉类型,type 不会创建新的类型
- interface 接口是声明数据结构的另一种方式,interface 仅限于声明对象类型和函数类型
相同点
- 都可以描述对象和函数
- 都允许扩展,interface 通过 extends 实现,type 通过 & 实现。interface 和 type 之间也可以相互扩展,此时type 必须是对象类型或对象交叉类型
- interface 和 type(联合类型除外)都可以被类实现
不同点
- type 可以声明基本类型别名、联合类型、元组和交叉类型,而 interface 做不到
- interface 可以重复声明,并会进行声明合并,而 type 重复声明会报错
泛型、泛型约束条件和 infer
泛型
泛型在程序中定义形式类型参数,在使用时用实际类型参数来替换。可以定义通用的数据结构,增加了代码类型的通用性。
泛型约束
泛型约束表示泛型类型必须是某个类型的子类型,通过 extends 实现,比如:
interface Point {
x: number
y: number
}
function toArray<T extends Point>(a: T, b: T): T[] {
return [a, b]
}
infer
infer的作用是在约束条件中推导泛型参数的类型,比如:
type ArrayElementType<T> = T extends (infer E)[] ? E : T
type t1 = ArrayElementType<number[]> // number
type t2 = ArrayElementType<{ name: string }> // {name: string}
any unknown never void 的区别
- any: 任意类型,是 ts 变量不写类型声明时的默认类型,不做任何约束,编译时会跳过对其的类型检查
- unknown: 未知类型,即写代码的时候不知道具体是怎样的类型,与 any 类似,比 any 更严格,在操作的时候,需要先进行类型断言或守卫。unknown 类型变量可以被任何类型赋值,而且它只能赋值给 any 或 unknown 类型变量
- never: 永不存在值的类型,例如函数总是抛出异常或执行死循环代码的返回值类型。变量也可以声明为 nerver 类型,除了 never 以外所有类型都不能赋值给它
- void: 无任何类型,表示函数没有返回值或者返回值是 undefined 或 null
typeof 和 keyof 的作用
- typeof 接受一个变量,获取变量的类型
- keyof 获取某个类型(比如接口或类)的所有属性名称,返回由属性名称组成的联合类型
TS 内置工具类型有哪些
- Partial 将对象类型的所有属性变为可选属性
- Required 将对象类型的所有属性变为必选属性
- Record<T, U> 定义对象类型的键和值的类型
- Readonly 定义对象类型的属性是只读的
- Pick<T, U> 挑选对象类型 T 中 U 对应的属性和类型
- Omit<T, U> 与 Pick 功能是互补的,挑选出对象类型 T 中不在 U 中的属性和类型
- Exclude<T, U> 从类型 T 中剔除所有 U 包含的类型,全部剔除会得到 never 类型
- Extract<T, U> 与 Exclude 的功能是互补的,从类型 T 中获取所有 U 包含的类型。
工具类型源码:
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type Exclude<T, U> = T extends U ? never : T;
type Extract<T, U> = T extends U ? T : never;
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
HTML/CSS
回流和重绘区别
区别:
- 回流指当前窗口发生改变,发生滚动操作,或者元素的位置大小相关属性被更新时会触发布局过程,发生在render树,比如元素的几何尺寸变化,就需要重新验证并计算Render Tree
- 重绘指当前视觉样式属性被更新时触发的绘制过程,发生在渲染层render layer
- 所以相比之下,回流的成本要比重绘高得多
减少回流重绘次数的方法:
- 避免一条一条的修改DOM样式,而是修改className或者style.classText
- 对元素进行一个复杂的操作,可以先隐藏它,操作完成后在显示
- 在需要经常获取那些引起浏览器回流的属性值时,要缓存到变量中
- 不使用table布局,一个小的改动可能就会引起整个table重新布局
- 在内存中多次操作节点,完成后在添加到文档中
margin 塌陷与合并问题
- margin 塌陷是指父子关系的两个元素,在垂直方向上的 margin 发生重叠,父元素的外边距取的是两个的最大值,发生塌陷的子元素随整个文档移动。
- margin 合并是两个兄弟元素之间,在垂直方向上的 margin 发生重叠,两者之间的外边距取的是两个的最大值。
- 可以通过触发 BFC 来解决,BFC 是一块独立的渲染区域,子元素的布局不会影响其他元素的布局,以下元素会触发 BFC:
- 根元素
- float 的属性不为 none
- position 的属性是 absolute 或 fixed
- display 的属性是 inline-block、flex、table-cell 或 table-caption
- overflow 的属性不为 visible
元素垂直水平居中
未知宽高的垂直水平居中
- flex 实现:
.parent {
display: flex;
justify-content: center;
align-items: center;
margin: auto;
width: 600px;
height: 600px;
border: 1px solid red;
}
.child {
width: 100px;
height: 100px;
border: 1px solid yellow;
}
- 绝对定位 + transform 实现:
.parent {
width: 600px;
height: 600px;
position: relative;
border: 1px solid red;
margin: auto;
}
.child {
position: absolute;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
transform: translate(-50%, -50%);
border: 1px solid yellow;
}
已知宽高的垂直水平居中
- 绝对定位 + margin: auto
.parent {
position: relative;
width: 600px;
height: 600px;
margin: auto;
border: 1px solid red;
}
.child {
position: absolute;
margin: auto;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100px;
height: 100px;
border: 1px solid blue;
}
- 绝对定位 + 负外边距
.parent {
position: relative;
width: 600px;
height: 600px;
margin: auto;
border: 1px solid red;
}
.child {
position: absolute;
top: 50%;
left: 50%;
margin: -50px 0 0 -50px;
width: 100px;
height: 100px;
border: 1px solid blue;
}
三栏布局,两侧定宽中间自适应
<div class="outer">
<div class="sider left"></div>
<div class="center"></div>
<div class="sider right"></div>
</div>
- float 实现
.outer {
width: 100%;
height: 600px;
border: 1px solid red;
}
.sider {
width: 200px;
height: 600px;
border: 1px solid blue;
box-sizing: border-box;
}
.left {
float: left;
}
.right {
float: right;
}
.center {
width: calc(100% - 400px);
height: 600px;
float: left;
}
- 定位实现
.outer {
position: relative;
}
.sider {
position: absolute;
top: 0;
height: 500px;
width: 200px;
background-color: lightskyblue;
}
.left {
left: 0;
}
.right {
right: 0;
}
.center {
/* 方案一:
margin: 0 200px 0 200px;
*/
/* 方案二:
position: absolute;
left: 300px;
width: cala(100% - 400px);
*/
/* 方案三: */
position: absolute;
left: 200px;
right: 200px;
height: 500px;
background-color: pink;
}
- flex 布局实现
.outer {
display: flex;
}
.sider {
width: 200px;
height: 500px;
background-color: lightskyblue;
}
.center {
order: 2; /* 默认值为0,order越小位置越靠前,越靠上,此时就不用考虑覆盖的问题了*/
flex-grow: 1;
height: 500px;
background-color: pink;
}
.left {
order: 1;
}
.right {
order: 3;
}
- grid 布局
.outer {
display: grid;
width: 100%;
grid-template-rows: 500px;
grid-template-columns: 200px auto 200px;
}
.sider {
background-color: lightskyblue;
}
.center {
background-color: pink;
}
cookie、localStorage、sessionStorage、indexedDB 的区别
- cookie
- 有效期默认是窗口会话的有效期,但可以手动设置过期时间
- cookie 会随请求发送到服务端,客户端服务端都可操作
- 存储大小 4k 左右
- localStorage
- 数据会一直存在,除非手动删除
- 在同源窗口下共享数据
- 存储大小 5M 左右
- sessionStorage
- 数据有效期是窗口会话的有效期
- 在同源窗口下共享数据
- 存储大小 5M 左右
- indexedDB
- 除非手动删除,否则一直有效
- 使用异步调用
- 支持事务
- 存储空间大,一般在不少于 250M
- 可以存储二进制数据
cookie 相关的属性
- Expires:cookie 的过期时间,值为 Date 类型,如果没有设置,默认是会话的有效期。
- Max-Age:表示 cookie 在多少秒之后过期,设置 0 或 -1 会直接过期,比 Expires 优先级高。
- Domain:表示 cookie 可以送达的域名,即有效范围,例如:.baidu.com。
- Path:指定一个路径,匹配的路径才可以使用对应的 cookie,下级目录也满足匹配的条件。
- Secure:表示仅在使用 https 协议的请求中被发送到服务端。
- HttpOnly:用于阻止 JS 访问 cookie。
- SameSite:设置 cookie 是否随着跨站请求一起发送。
- Strict:仅对同一站点的请求发送 cookie。
- Lax:允许与顶级导航一起发送,并将与第三方网站发起的 GET 请求一起发送,这是浏览器中的默认值。
- None:在跨站和同站请求中均发送 cookie,必须同时设置 Secure 属性。
Vue
Vue 双向数据绑定原理
Vue2 实现原理
Vue2 双向数据绑定由 数据监听器 Observer + 模版编译器 Compiler 构成。 数据监听器由:Object.defineProperty() + 依赖收集 Dep 实现。 主要流程是:
- 组件实例执行初始化,对 data 执行响应式处理。
- 通过 Object.defineProperty() 实现 defineReactive() 方法。
- 为 data 的每个 key 创建一个 Dep 实例,并实现 get 和 set 方法。如果 key 对应初始值是对象或数组,会执行递归响应式处理。
- 在 get 中判断 Dep.target 如果不为空,就将 Watcher 实例添加到 Dep 实例的管理列表中,并返回 key 的新值。
- set 方法用于更新 key 的值,并通过它的 Dep 通知其管理的所有 Watcher 执行更新函数。
- 对于数组类型数据,重写数组原型中 7 个可修改数组自身的方法:push、pop、unshift、shift、splice、sort、reverse,重写后的方法里加入了通知 Watcher 更新的逻辑,并将修改后的原型赋给数组的 __proto__属性。
- 对模版执行编译,找到其中动态绑定的数据,从 data 中获取并初始化视图,同时定义一个 Watcher,并将其添加到对应 Dep 的管理列表。
- 由于 data 的某个 key 在模版中可能被多个节点绑定,所以每个 key 都需要依赖收集 Dep 来管理多个 Watcher。
- 以后 data 中的数据一旦发生变化,会首先找到对应的 Dep,通知其管理的所有 Watcher 执行更新函数。
流程图如下:
Vue3 双向数据绑定和 Vue2 的区别
- Vue2 通过 Object.defineProperty() 来劫持对象属性的 getter 和 setter 操作,Vue3 通过 Proxy 来劫持数据。
- Proxy 在对象级别做监听,defineProperty 只能监听某个属性,不能对全对象监听。
- Proxy 可以监听数组,无需再对数组原型方法做特殊处理。
- Proxy 模式下,能检测到对象属性的添加和删除。
编译器 Compiler 的作用
- 递归处理模板中的节点,检测到元素有 v-bind 开头的指令或者双大括号指令,就会从 data 中取对应的值去修改模板内容,同时创建一个 watcher 添加到该属性的 dep 管理列表中。
- 遇到 v-on 开头的指令,会对该节点添加对应事件的监听,并使用 call 方法将 vue 绑定为该方法的 this。
- 最终将模版编译为抽象语法树 AST,然后以 AST 作为参数调用 render 函数生成虚拟 DOM。
Vue 的 diff 算法
Vue2 实现原理
- Vue 的 diff 算法通过对新旧虚拟 DOM 做对比,精确地找出之间的差异,最小化更新真实 DOM,此过程称为 patch。
- diff 过程遵循深度优先、同层比较、首尾指针的策略,比对过程以新的 VNode 为准。
- 首先判断是否是同类标签,如果不是同类标签,直接替换。
- 如果是同类标签,判断是否相等,如果相等直接 return,否则比对子节点。
- 旧的有子节点、新的没有子节点的情况,直接清空旧的子节点。
- 旧的没有子节点、新的有子节点的情况,直接添加新的子节点。
- 子节点是文本的情况,用新的文本替换旧文本。
- **都有子节点的情况,**通过头头、尾尾、头尾、尾头依次比对查找,如果相同则移动到新虚拟节点的位置,头尾指针向中间移动,直到头部索引超出尾部索引为止。如果都没找到,就通过 key 在旧虚拟节点中查找 key 值相同的节点进行复用。
Vue3 中对 diff 的优化
- 事件缓存:将事件缓存,优先从缓存中获取。
- 添加静态标记:Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff。
- 静态提升:创建静态节点时保存,后续直接复用。
- 使用最长递增子序列优化对比流程:Vue2 里在 updateChildren() 函数里对比变更,在 Vue3 里这一块的逻辑主要在 patchKeyedChildren() 函数里。
key 的作用
Vue 在 patch 过程中通过 key 可以精准判断两个节点是否是同一个,从而避免频繁更新不同元素,使得整个 patch 过程更加高效。
对虚拟 DOM 的理解
虚拟 DOM 是对真实 DOM 的抽象,是以 JS 对象为基础的树形结构,用对象的属性描述节点,属性至少包含:标签名、属性、子元素。 最终通过一系列操作将虚拟 DOM 映射到真实页面中。 为什么使用虚拟 DOM:
- 操作真实 DOM 是很慢的,其元素非常庞大,频繁操作会出现页面卡顿,页面的性能问题,大多是由 DOM 操作引起的。
- 通过虚拟 DOM 以及 diff 算法可以实现局部更新真实 DOM,避免了大量的 DOM 操作。
Vue2 和 Vue3 的区别
新增的特性
- 组合式 API 及单文件组件组合式 API 语法糖
- Teleport 组件,可将组件模版挂载到指定的 DOM 节点上
- Fragments 片段,组件模版支持多根节点
- emits 选项,用于声明组件内部触发的自定义事件,如果没有声明,会将对应事件监听器透传到组件根 DOM 元素上
- 单文件组件样式属性可以使用 v-bind() 绑定动态值
- 但文件组件样式增加全局规则和针对插槽内容的规则
- 深度选择器::deep(.foo) {}
- 针对插槽内容::slotted(.foo) {}
- 全局样式::global(.foo) {}
- 单文件组件样式增加 module 模式
- Suspense 组件,可以在异步组件加载完成前显示加载中的内容
更改的特性
- 使用 createApp() 返回一个应用实例,Vue 下的一些全局 API 统一移动到应用实例上
- Vue.config -> app.config
- Vue.config.productionTip -> 移除
- Vue.config.ignoredElements -> app.config.compilerOptions.isCustomElement
- Vue.component -> app.component
- Vue.derective -> app.derective
- Vue.mixin -> app.mixin
- Vue.use -> app.use
- Vue.prototype -> app.config.globalProperties
- Vue.extend -> 移除,始终使用 createApp() 或 defineComponent()
- app.provide('key', 'val') -> 新增
- 一些全局 API 作为全局具名导出进行访问
- Vue.nextTick
- Vue.version
- Vue.set -> 移除
- Vue.delete -> 移除
- h 函数从全局导入,另外渲染函数第二个参数 props 现在是扁平化的对象结构,并且将 slot 属性移动到第三个参数
- 内置组件:Transition、Teleport 等
- v-model 默认属性和事件有更改,Vue3 组件可以有多个 v-model,以传参方式实现
- key 属性相关
- v-if / v-else / v-else-if 的各分支项 key 将不再是必须的
- template v-for 的 key 应该设置在 template 标签上 (而不是设置在它的子节点上)
- v-if 与 v-for 两者作用于同一个元素上时,v-if 会拥有比 v-for 更高的优先级
- 同时使用 v-bind="object" 语法和独立 attribute 的场景,后绑定的会覆盖先绑定的
- 函数式组件移除 functional 属性
- 使用 defineAsyncComponent() 定义异步组件
- this.scopedSlots),统一移动到 this.$slots 中
- $attrs 包含 class 及 style
- Vue3 中对 is attribute 的特殊处理限制在了 组件中,其他组件仅作为普通属性传入,在 2.0 中需要添加 vue: 前缀
- 指令的钩子函数已经被重命名,与组件的生命周期保持一致。另外,expression 字符串不再作为 binding 对象的一部分被传入。
- created - 新增!在元素的 attribute 或事件监听器被应用之前调用。
- bind → beforeMount
- inserted → mounted
- beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。
- update → 移除!该钩子与 updated 有太多相似之处,因此它是多余的。请改用 updated。
- componentUpdated → updated
- beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。
- unbind -> unmounted
- 组件选项 data 的声明不再接收纯 JavaScript object,而是接收一个 function
- 当合并来自 mixin 或 extend 的多个 data 返回值时,合并操作现在是浅层次的而非深层次的 (只合并根级属性)。
- 使用 mount() 挂载的组件将不会替换目标元素,而是将渲染内容插入到目标元素
- props 选项中不能再使用 this
- VNode 声明周期事件更新,比如 @hook:updated -> @vue:updated
- 生命周期:beforeDestroy -> beforeUnmount,destroyed -> unmounted
- watch 侦听数组变化时,需要加上 deep 属性
- 双向数据绑定,Vue3 使用 Proxy 代替了 Object.defineProperty()
- Vue3 在 diff 算法上做了优化,使用静态标记、缓存事件及最长递增子序列的优化方法
删除的特性
- .sync
- .native(Vue3 中 emits 没声明的事件,均被认为是原生事件)
- **attrs,以 on 为前缀
- Vue.config.keyCodes 移除,另外不再支持数字形式的事件修饰符
- 在组件实例中完全移除了 off、$once 方法,EventBus 可通过 tiny-emitter 库实现
- 移除过滤器 filters
- 移除 $children,可用 ref 代替
- propsData
新的推荐
- 新版本的 vue-router、devtools
- 构建工具链 Vite,如果使用自定义的 webpack 设置,需将 vue-loader 升级至 ^16.0.0
- 状态管理 Pinia
- IDE 支持 Volar
- 新的 TS 命令行工具 vue-tsc
- 静态网站生成 VitePress
- JSX:@vue/babel-plugin-jsx
Vue 组件间通信方式
- 父子组件通信
- 父传子使用 props、ref
- 子传父使用 parent、v-model、.sync (vue3已废弃)
- 兄弟组件通信
- EventBus
- 通过共同父组件
- 祖先与后代组件通信
- $attrs
- provide、inject
- vuex
- $root
- 非关系组件
- vuex
- EventBus
- $root
对 v-model 及 .sync 的理解
- v-model 和 .sync 的作用是实现父子组件之间的双向数据绑定。
- 在 vue2 中,定义 model 对象的 prop (默认 value)和 event (默认 input) 属性,并在组件内 **$emit **对应的event,以实现 v-model 双向数据绑定,一个组件仅能声明一个 v-model。
- .sync 是对组件的 prop 进行双向绑定,他是 update 事件的语法糖。
- 在 vue3 中, 不用再使用 model 对象定义 v-model,改为直接使用 prop 和 emits 中的属性和事件, v-model 的默认值为 modelValue 和 update:modelValue,并且可以声明多个 v-model。
- vue3 废弃了 .sync,改为使用 v-model:xxx 参数形式。
- computed 计算属性使用 v-model 的值,必须同时定义 get 和 set,否则会报错。
Vue 封装公共组件的注意事项
- 要保证组件的通用性、低耦合性
- 对传入的 props 添加类型校验,不能修改 props 的值
- 组件中的事件处理尽量通知到外层,让父组件去处理,避免组件中过多的事件处理逻辑
- 合理使用全局和局部 css 样式
- 可以使用 slot 提供给用户自定义组件内容
Vue 父子组件生命周期的执行顺序
- 父beforeCreate -> 父created -> 父beforeMounted -> 子beforeCreate -> 子created -> 子beforeMounted -> 子mounted -> 父mounted
- 父beforeUpdate->子beforeUpdate->子updated->父updated
- 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
computed 与 watch 的实现原理及区别
computed 的实现原理
computed 计算属性一般用于依赖一个或多个属性返回一个新的计算后的属性值,具有缓存的特性,如果依赖的属性没有发生变化,会从缓存中取值。 主要原理:
- 组件执行初始化,为所有计算属性分别创建 watcher,将组件实例 vm 和对应 get 方法作为参数传入,将 lazy 和 dirty 参数置为 true,并将 watcher 分别添加到依赖的属性的 dep 管理列表中。
- 使用 **Object.defineProperty() **对每个计算属性进行响应式处理,重写 get 方法,判断对应 watcher 的 dirty 值,如果为 false,直接返回缓存的 value 值,否则重新计算新值,并将 dirty 置为 false。
- 如果依赖的属性发生变化,dep 通知 watcher 执行 update 方法,将 dirty 的值置为 true。
watch 的实现原理
watch 一般用于监听一个属性的变化,并执行一些业务逻辑。 主要原理:
- 创建一个 watcher,将 watcher 添加到监听的属性的 dep 管理列表中。
- 当属性值发生变化,会通知到 watcher 执行回调函数。
ref 和 reactive 的区别
- ref 和 reactive 都是用来定义响应式数据。
- ref 适合定义基本类型数据,也可定义复杂类型数据。
- reactive 只能用来定义复杂类型数据(对象或数组)。
- ref 使用类的 getter 和 setter 实现,参数如果是复杂类型,会借用 reactive 来实现。
- reactive 本质是将传入的数据包装成一个 Proxy。
toRef 和 toRefs 的区别
- toRef:为响应式对象的某个属性创建 ref,并与原对象属性保持引用关系。
- toRefs:为响应式对象的每个属性创建 ref,并与原对象属性保持引用关系。
如何在组合式 API 中使用全局属性
使用 app.provide() 与 inject()
在应用层面使用 **app.provide() **方法向所有后代组件提供数据,并在后代组件中使用 inject() 方法注入对应数据。例如 **app.provide('$echarts', echarts) **向所有后代组件提供 echarts:
import { createApp } from 'vue'
import App from './App.vue'
import * as echarts from 'echarts'
const app = createApp(App)
app.provide('$echarts',echarts) // 全局挂载echarts
app.mount('#app')
后代组件 inject('$echarts') 注入 echarts:
<script setup>
import { inject, onMounted } from 'vue'
const echarts = inject('$echarts') 导入挂载的echarts
onMounted(() => {
let myChart = echarts.init(document.getElementById('main'))
console.log(myChart)
});
</script>
使用 globalProperties 与 getCurrentInstance()
例如,在应用层面使用 app.config.globalProperties.$echarts 提供全局的 echarts:
import { createApp } from 'vue'
import App from './App.vue'
import * as echarts from 'echarts'
const app = createApp(App)
app.config.globalProperties.$echarts = echarts //全局挂载echarts属性
app.mount('#app')
在后代组件通过 getCurrentInstance() 使用 echarts:
<script setup>
import { getCurrentInstance } from 'vue'
const { proxy } = getCurrentInstance()
proxy.echarts
</script>
组合式函数
组合式函数是利用 Vue3 的组合式 API 来封装和复用有状态逻辑的函数,组合式函数约定用驼峰命名法命名,并以 use 作为开头。例如用于维护数据请求状态的 useFetch:
// fetch.js
import { ref, isRef, unref, watchEffect } from 'vue'
export function useFetch(url) {
const data = ref(null)
const error = ref(null)
function doFetch() {
// 在请求之前重设状态...
data.value = null
error.value = null
// unref() 解包可能为 ref 的值
fetch(unref(url))
.then((res) => res.json())
.then((json) => (data.value = json))
.catch((err) => (error.value = err))
}
if (isRef(url)) {
// 若输入的 URL 是一个 ref,那么启动一个响应式的请求
watchEffect(doFetch)
} else {
// 否则只请求一次
// 避免监听器的额外开销
doFetch()
}
return { data, error }
}
React
React 的特性有哪些
- JSX 语法
- 单向数据流
- 虚拟 DOM
- 声明式编程
- Component
- Hook
对 React Hook 的理解,使用过哪些 Hook
Hook 是 React 16.8 版本中引入的特性
- 可以在函数组件中使用 state 及其他 React 特性
- 可以重用组件中的状态逻辑
- 不存在 this 指向问题
使用过的 Hook:
- React 内置:useState、useEffect、useContext、useRef、useMemo、useCallback
- 路由相关:useLocation、useParams、useNavigate
Hook 等使用限制:Hook 必须在函数组件顶层,或者在自定义 Hook 内使用。
React 的 diff 算法
React 的 diff 算法主要遵循三个层级的策略:
- tree 层级:只比较同一层级的节点,忽略跨层级的操作,只有删除、创建操作,没有移动操作。
- component 层级:在对比两个组件时,如果是同一类的组件,则会继续往下 diff 运算,如果不是一个类的组件,那么直接删除旧组件,创建新的。
- element 层级:对于同一层级的一组节点,会使用具有唯一性的 key 来区分是否需要创建、删除、或者移动。
对 Fiber 的理解
- React 15 渲染过程不可中断并且是同步执行的,在渲染复杂场景组件时会出现页面卡顿问题。为解决此问题,在 React 16 版本中引入了 Fiber 新的协调引擎。
- Fiber 是一种数据结构,用 JS 的对象来表示。
- Fiber 对渲染任务进行拆分,执行增量渲染。
- 通过 Fiber 可以暂停、终止、复用渲染任务。
- Fiber 对不同的任务赋予不同的优先级,高优先级的任务优先渲染。
PureComponent 与 memo
- PureComponent 用于类组件,对 props 和 state 的新旧值进行浅层对比,如果没有变化,会跳过重新渲染。
- PureComponent 是 Component 的子类,默认实现了 shouldComponentUpdate 钩子方法。
- PureComponent 类似函数组件的 memo, memo 默认对新旧 props 进行浅层对比,如果没有变化,会跳过重新渲染。
- 可以指定 memo 的第二个参数,以自定义新旧 props 的对比规则,相等返回 true,否则返回 false。
- 在函数组件里,对于类型为对象、数组或函数的 prop,尽量用 useMemo 和 useCallback 缓存其定义。
useMemo 和 useCallback 的区别
- useMemo 接收两个参数:无参数的回调函数、依赖数组。缓存回调函数的返回值,当依赖没有变化时,返回缓存的值。
- useCallback 接收两个参数:任意参数的回调函数、依赖数组。缓存回调函数,当依赖没有变化时,返回缓存的回调函数。
- 当 useMemo 的回调函数返回的值是函数时,可以替代 useCallback。
- useMemo 和 useCallback 结合 memo 可以避免组件不必要的重新渲染。
高阶组件(HOC)
高阶组件是一个函数,它接收组件作为参数,并返回一个新组件。高阶组件通常用于:
- 复用逻辑
- 控制渲染
- 状态管理
- 权限控制
比如创建一个 withLoading 的高阶组件:
import React from "react';
export default function withLoading(WrappedComponent) {
return function WithLoadingComponent({ isLoading, ...props }) {
return isLoading ? <div>loading</div> : <WrappedComponent {...props} />
}
}
使用 withLoading 高阶组件:
import React from 'react';
import withLoading from './withLoading';
function DataList({ data }) {
return (
<ul>
{data.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
export default withLoading(DataList);
前端工程化
对前端工程化的理解
前端工程化是为了提高开发效率降低重复性的工作提出的。包括:模块化、组件化、规范化及自动化四方面。
- 模块化
- 模块化是将一个大的文件拆分成多个相互依赖的小文件,按一个个模块来划分
- 组件化
- 页面是个大型组件,可以拆分成若干个中型组件,中型组件也可以再拆分若干个小型组件
- 组件化不等于模块化,模块化是在文件级别对代码和资源做拆分,组件化是在设计层面对 UI 的拆分
- 规范化
- 目录结构的制定
- 编码规范
- 前后端接口规范
- 文档规范
- 组件管理
- Git 分支管理
- Commit 描述规范
- 定期 code-review
- 视觉图标规范
- 自动化
- 持续集成
- 自动化构建
- 自动化部署
- 自动化测试
webpack
webpack 的构建流程
webpack 的构建流程是一个串行的过程,从启动到结束会依次执行以下步骤:
- 初始化参数:从配置文件和 shell 语句中读取与合并参数,得到最终的参数。
- 开始编译:用上一步得到的参数初始化 compiler 对象,加载所有配置的插件,通过执行对象的 run 方法开始编译。
- 确定入口:根据配置中的 entry 找出所有的入口文件。
- 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行处理,再找出该模块依赖的模块,递归当前步骤直到依赖的所有模块都经过处理。
- 完成模块编译:经过上一步之后,得到每个模块被处理后的最终内容以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再将每个 chunk 转换成单独的文件加入到输出列表中。
- 输出完成:确定好输出内容后,根据配置确定输出的路径和文件名,将文件内容写入到文件系统中。
compiler 与 compilation 对象
webpack 编译会创建两个核心对象:
- compiler:包含了 webpack 环境的所有的配置信息,包括 options,loader 和 plugin,和 webpack 整个生命周期相关的钩子
- compilation:作为 plugin 内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的 compilation 将被创建
对 Loader、Plugin 的理解,两者的区别
- loader: webpack 只能理解 JavaScript 和 JSON 文件。loader 使 webpack 能够处理其他类型的文件,并将它们转换为有效模块,并添加到依赖图中。loader 在 module.rules 中配置,最先声明的 loader 最后执行。
- css-loader:对 @import 和 url() 进行处理,使其变为 webpack 可识别的模块
- style-loader:将 css 插入到 dom 中
- ts-loader:将 TS 文件转换成 JS 模块并输出
- babel-loader:转译 js 和 jsx 文件
- less-loader:将 less 编译为 css
- postcss-loader:处理 css,比如添加浏览器前缀
- plugin: 插件可以扩展 webpack 功能,用于执行范围更广的任务。包括:打包优化,资源管理,注入环境变量。插件在 plugins 中单独配置,类型为数组,每一项是一个插件的实例,参数都通过构造函数传入。
- html-webpack-plugin:生成 HTML 模版文件,通过 script 标签引入 webpack 生成的 bundle
- DefinePlugin:内置插件,可以定义各种变量,在代码中使用
- copy-webpack-plugin:从一个目录拷贝静态资源文件到指定目录
- terser-webpack-plugin:用于生产模式下压缩 JS 代码
- mini-css-extract-plugin:将 css 提取到单独的文件中,为每个包含 css 的 JS 文件创建一个 css 文件
- css-minimizer-webpack-plugin:压缩 css 代码
- BannerPlugin:内置插件,可以在 chunk 文件头部创建描述信息
编写 Loader 和 Plugin
编写 Loader:
- Loader 本质是一个函数,函数接收一个参数,为传递给当前 Loader 的文件源内容
- 函数中 this 是由 webpack 提供的对象,能够获取当前 Loader 所需的各种信息
- 不能将 Loader 定义成箭头函数
- 函数中异步操作使用 this.callback(null, content) 返回,同步操作则直接 return content
- 返回值是 string 或 buffer 类型
编写 Plugin:
- Plugin 必须是一个函数或类,且必须有一个 apply 方法,参数为 compiler 对象
- 在 apply 方法中,利用 compiler 对象注册指定的事件钩子函数,钩子函数的参数为编译对象 compilation
- 异步的事件需要在插件处理完任务时调用回调函数通知 webpack 进入下一个流程,不然会卡住
自定义 Plugin
做过哪些 webpack 打包优化
- JS 代码压缩
- CSS 代码压缩
- HTML 文件代码压缩
- Tree Shaking
- 代码分割,按需加载
- 提取公共代码
- 使用 CDN 加载资源,配置 output.publicPath 属性
webpack 的热更新
模块热更新(HMR),指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用:
- 通过 webpack-dev-server 创建两个服务器:提供静态资源的服务(express)和 socket 服务。
- express server 负责直接提供静态资源(打包后的资源直接被浏览器请求和解析)。
- socket server 是一个 websocket 的长连接,双方可以通信。
- 当 socket server 监听到对应的模块发生变化时,会生成两个文件.json(manifest文件)和 .js 文件(update chunk)。
- 通过长连接,socket server 可以直接将这两个文件主动发送给客户端(浏览器)。
- 浏览器拿到两个新的文件后,通过 HMR runtime 机制,加载这两个文件,并且针对修改的模块进行更新。
webpack 与 vite 的区别
- webpack 会先打包,然后启动开发服务器,请求服务器时直接给予打包结果。 而 vite 是直接启动开发服务器,请求哪个模块再对该模块进行实时编译。 由于现代浏览器本身就支持 ESModule,会自动向依赖的 module 发出请求。vite 充分利用这一点,将开发环境下的模块文件,就作为浏览器要执行的文件,而不是像 webpack 那样进行打包合并。
- 由于 vite 在启动的时候不需要打包,也就意味着不需要分析模块的依赖、不需要编译,因此启动速度非常快。当浏览器请求某个模块时,再根据需要对模块内容进行编译。这种按需动态编译的方式,极大的缩减了编译时间,项目越复杂、模块越多,vite 的优势越明显。
- 在 HMR 方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,不像 webpack 那样需要把该模块的相关依赖模块全部编译一次,效率更高。
- 当需要打包到生产环境时,vite 使用传统的 rollup 进行打包,因此,vite 的主要优势在开发阶段。另外,由于 vite 利用的是ESModule,因此在代码中不可以使用 CommonJS。
模块化
ESModule 和 Commonjs 的区别
- 两者的模块导入导出语法不同,ESModule 是通过 export 导出,import 导入,CommonJs 是通过 module.exports,exports 导出,require 导入。
- ESModule 在编译期间会将所有 import 提升到顶部,CommonJs 不会提升 require。
- CommonJs 是运行时加载模块,ESModule 是在静态编译期间就确定模块的依赖。
- CommonJs 导出的是一个值拷贝,会对加载结果进行缓存,一旦内部再修改这个值,则不会同步到外部。ESModule 是导出的一个引用,内部修改可以同步到外部。
- CommonJs 中顶层的 this 指向这个模块本身,而 ESModule 中顶层 this 指向 undefined。
- CommonJS 加载的是整个模块,将所有的接口全部加载进来,ESModule 可以单独加载其中的某个接口。
HTTP/网络/安全
从地址栏输入 URL 到呈现页面
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 建立 TCP 连接(三次握手);
- 浏览器发出读取文件的 HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 HTML 文本发送给浏览器;
- 浏览器解析该 HTML 文本并显示内容;
- 将 HTML 解析成 DOM 树
- 将 CSS 解析成 CSSOM 树
- 遇到
- 根据 DOM 树和 CSSOM 树构建 Render 树
- 根据 Render 树进行布局渲染 Render Layer,计算好每个节点的位置和尺寸,将其放在浏览器窗口的正确位置
- 根据计算的布局信息进行绘制,将最终内容显示在屏幕上
- 释放 TCP 连接(四次挥手)。
HTTP 方法有哪些,GET 和 POST 区别有哪些
HTTP1.0 定义了三种请求方法,GET,POST 和 HEAD 方法 HTTP1.1 新增六种请求方法:OPTIONS,PUT,PATCH,DELETE,TRACH 和 CONNECT GET 和 POST 区别主要从三方面讲即可:参数表现形式,传输数据大小,安全性。
- 首先表现形式上:GET 请求的数据会附加在 URL 后面,以 ? 分割,多参数用 & 连接,可缓存;POST 请求会把请求参数放在请求体中,不可缓存
- 传输数据大小:对于 GET 请求,HTTP 规范没有对 URL 长度进行限制,但是不同浏览器对 URL 长度加以限制,所以 GET 请求时,传输数据会受到 URL 长度的限制;POST 不是 URL 传值,理论上无限制,但各个服务器一般会对 POST 传输数据大小进行限制
- 安全性:相比 URL 传值,POST 利用请求体传值安全性更高
HTTP的缓存过程(强缓存和协商缓存)
- 通过头部信息 Cache-Control 和 Expires 判断资源是否过期,如果时间未过期,则直接从缓存中取,即强缓存
- Cache-Control
- 其中
max-age = <seconds>设置缓存存储的最大周期,超过这个时间缓存将会被认为过期,时间是相对于请求的时间 - public 表示响应可以被任何对象缓存,即使是通常不可缓存的内容
- private 表示缓存只能被单个用户缓存,不能作为共享缓存(即代理服务器不可缓存它)
- no-cache 告诉浏览器、缓存服务器,不管本地副本是否过期,使用副本前一定要到源服务器进行副本有效校验
- no-store 缓存不应该存储有关客户端请求或服务器响应的任何内容
- 其中
- Expires
- Expires 字段规定了缓存的资源的过期时间,该字段时间格式使用 GMT 标准时间格式, js 通过
new Date().toUTCString()得到,由于时间期限是由服务器生成,存在着服务端和客户端的时间误差,相比 Cache-Control 优先级较低
- Expires 字段规定了缓存的资源的过期时间,该字段时间格式使用 GMT 标准时间格式, js 通过
- Cache-Control
- 那么如果判断缓存时间已经过期,将会采用协商缓存策略
- 那么浏览器在发起 HTTP 请求时,会带上 **If-None-Match 和 If-Modified-Since **请求头,其值分别是上一次发送 HTTP 请求时,服务器设置在 Etag 和 Last-Modified 响应头中的值
- 请求头中的 If-None-Match 将会和资源的 Etag 进行对比,如果不同,则说明资源被修改过,响应 200 并返回最新资源;如果相同,则说明资源未改动,响应 304
- 如果请求头中没有 If-None-Match,则会判断 If-Modified-Since,如果资源最后修改时间大于 If-Modified-Since,说明资源被改动过,响应完整资源内容,返回状态码 200;如果小于或者等于,说明资源未被修改,则响应状态码 304,告知浏览器可以继续使用所保存的缓存
HTTP的状态码,301 和 302 的区别
状态码告知从服务器返回请求的状态,一般由以 1~5 开头的三位整数组成: 1xx:请求正在处理 2xx:成功 3xx:重定向
- 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
- 302 found,临时性重定向,表示资源临时被分配了新的 URL
- 303 see other,表示资源存在着另⼀个 URL,应使⽤ GET ⽅法定向获取资源
- 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
- 307 temporary redirect,临时重定向,和 302 含义相同
4xx:客户端错误
- 400 bad request,请求报文存在语法错误
- 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息
- 403 forbidden,表示对请求资源的访问被服务器拒绝
- 404 not found,表示服务器上没找到请求的资源
- 408 Request timeout,客户端请求超时
- 409 confict,请求的资源可能引起冲突
5xx:服务器错误
- 500 internal server error,表示服务器端在执行请求时发生了错误
- 501 Not Implemented,请求超出服务器能力范围
- 503 service unavailable,表示服务器暂时处于超负载或正在停机维护,无法处理
- 505 http version not supported,服务器不支持,或者拒绝支持在请求中的使用的 HTTP 版本
同样是重定向,302 是 HTTP1.0 的状态码,在 HTTP1.1 版本的时候为了细化 302 又分出来 303 和 307,303 则明确表示客户端应采用 get 方法获取资源,他会把 post 请求变成 get 请求进行重定向;307 则会遵照浏览器标准,不会改变 post。
HTTP1.1 相比 1.0 的区别有哪些
HTTP1.1,在 1.0 的基础上主要增加了 5 项功能:
- 连接方式:HTTP1.0 使用短连接(非持久连接),HTTP1.1 默认采用带流水线的长连接(持久连接)
- 缓存方面:HTTP1.1 新增例如 ETag,If-None-Match,Cache-Control 等更多的缓存控制策略
- range 头域:HTTP1.0 在断连后不得不下载完整的包,存在一些带宽浪费的现象;HTTP1.1 则支持断点续传的功能,在请求消息中加入 range 头域,允许请求资源的某个部分,在响应消息中 Content-Range 头域中声明了返回这部分对象的偏移值和长度
- host 头域:在 HTTP1.0 中每台服务器都绑定一个唯一的 IP 地址,所有传递消息中的 URL 并没有传递主机名;HTTP1.1 请求和响应消息都应支持 host 头域,且请求消息中没有 host 头域名会抛出一个错误(400 Bad Request)
- 状态提示:HTTP1.1 新增 24 个状态响应码,比如 409(请求的资源与资源当前状态冲突),410(服务器上某个资源被永久性删除);相比 HTTP1.0 只定义了 16 个状态响应码
HTTP2.0 相比 HTTP1.1 的优势和特点
HTTP2.0 在 1.1 基础上增加了 4 项功能:
- 二进制协议:HTTP2 使用二进制格式传输数据,而 HTTP1.1 使用文本格式,二进制协议解析更高效
- 头部压缩:HTTP1.1 每次请求和发送都携带不常改变的,冗杂的头部数据,给网络带来额外负担;而HTTP2在客户端和服务器使用"部首表"来追踪和存储之前发送的键值对,对于相同的数据,不再每次通过每次请求和响应发送
- 服务端推送:服务端可以在发送页面 HTML 时主动推送其他资源,而不用等到浏览器解析到相应位置时,发起请求再响应
- 多路复用:在 HTTP1.1 中如果想并发多个请求,需要多个 TCP 连接,并且浏览器为了控制资源,一般对单个域名有 6-8 个 TCP 连接数量的限制;而在 HTTP2 中:
- 同个域名所有通信都在单个连接下进行
- 单个连接可以承载任意数量的双向数据流
- 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送
HTTP3.0(HTTP over QUIC)
HTTP3.0 也称为 HTTP over QUIC,核心是 QUIC 协议,QUIC 是 Quick UDP Internet Connections 的缩写,是基于 UDP 的协议。QUIC 底层通过 UDP 协议替代了 TCP,上层只需要一层用于和远程服务器交互的 HTTP/2 API。这是因为 QUIC 协议已经包含了多路复用和连接管理,HTTP API 只需要完成 HTTP 协议的解析即可。QUIC 协议的主要目的是整合 TCP 协议的可靠性和 UDP 协议的速度和效率。 QUIC 协议主要解决了以下问题:
- 减少了 TCP 三次握手及 TLS 握手时间,基于 UDP 协议的 QUIC,因为 UDP 本身没有连接的概念,连接建立时只需要一次交互,半个握手的时间
- 避免多路复用丢包的线头阻塞问题:QUIC 保留了 HTTP2.0 多路复用的特性,在之前的多路复用过程中,同一个 TCP 连接上有多个 stream,假如其中一个 stream 丢包,在重传前后的 stream 都会受到影响,而QUIC 中一个连接上的多个 stream 之间没有依赖。所以当发生丢包时,只会影响当前的 stream,也就避免了线头阻塞问题。
- 优化重传策略:以往的 TCP 丢包重传策略是:在发送端为每一个封包标记一个编号(sequence number),接收端在收到封包时,就会回传一个带有对应编号的 ACK 封包给发送端,告知发送端封包已经确实收到。当发送端在超过一定时间之后还没有收到回传的 ACK,就会认为封包已经丢失,启动重新传送的机制,复用与原来相同的编号重新发送一次封包,确保在接收端这边没有任何封包漏接。这样的机制就会带来一些问题,假设发送端总共对同一个封包发送了两次(初始+重传),使用的都是同一个 sequence number 编号 N。之后发送端在拿到编号N封包的回传 ACK 时,将无法判断这个带有编号 N 的 ACK,是接收端在收到初始封包后回传的 ACK。这就会加大后续的重传计算的耗时。QUIC 为了避免这个问题,发送端在传送封包时,初始与重传的每一个封包都改用一个新的编号,unique packet number,每一个编号都唯一而且严格递增,这样每次在收到 ACK 时,就可以依据编号明确的判断这个 ACK 是来自初始封包或者是重传封包。
- 流量控制:通过流量控制可以限制客户端传输资料量的大小,有了流量控制后,接收端就可以只保留相对应大小的接收 buffer ,优化记忆体被占用的空间。但是如果存在一个流量极慢的 stream ,光一个 stream 就有可能估用掉接收端所有的资源。QUIC 为了避免这个潜在的 HOL Blocking,采用了连线层 (connection flow control) 和 Stream 层的 (streamflow control) 流量控制,限制单一 Stream 可以占用的最大 buffer size。
- 连接迁移:TCP连接基于四元组(源IP、源端口、目的IP、目的端口),切换网络时至少会有一个因素发生变化,导致连接发生变化。当连接发生变化时,如果还使用原来的 TCP 连接,则会导致连接失败,就得等原来的连接超时后重新建立连接,所以我们有时候发现切换到一个新网络时,即使新网络状况良好,但内容还是需要加载很久。如果实现得好,当检测到网络变化时立刻建立新的 TCP 连接,即使这样,建立新的连接还是需要几百毫秒的时间。QUIC 的连接不受四元组的影响,当这四个元素发生变化时,原连接依然维持。QUIC 连接不以四元组作为标识,而是使用一个 64 位的随机数,这个随机数被称为 Connection lD,对应每个stream,即使 IP 或者端口发生变化,只要 Connection ID 没有变化,那么连接依然可以维持。
QUIC 面临的问题:
- 路由封杀 UDP 443 端口
- UDP 包过多,会被服务商误认为是攻击,UDP 包被丢弃
- 无论是路由器还是防火墙目前对 QUIC 都还没有做好准备
HTTPS 和 HTTP 的区别
HTTP 都是使用明文传输的,对于敏感信息的传输就很不安全,HTTPS 正是为了解决 HTTP 存在的安全隐患而出现的。当使用 HTTPS 时, 先和 SSL 通信,再由 SSL 和 TCP 通信。 HTTPS 采⽤了⾮对称加密和对称加密两者并⽤的混合加密机制。
- 浏览器与服务端握⼿,发送⾃⼰⽀持的加密算法。
- 服务端返回选择的加密算法和证书。
- 浏览器验证证书的合法性。颁发证书的机构是否合法,证书中包含的⽹站地址是否与正在访问的地址⼀致。
- 浏览器⽣成随机数的密码,并⽤证书中的公匙加密发送到服务端,服务端使⽤私匙解析获取随机数密码(⾮对称加密)。
- 浏览器和服务端使⽤之前⽣成的随机数进⾏对称加密数据,并在互联⽹上传输。
CDN
CDN (内容分发⽹络)的实现依赖多种⽹络技术的⽀持,主要包括负载均衡技术、动态内容分发与复制技术、缓存技术等。其核⼼思想是在⽤户和服务器之间增加 Cache 层,通过接管 DNS 的实现,将⽤户的请求引导 Cache 上获取资源,从⽽降低资源的响应时间。 CDN 的原理是,给源站点添加别名( cname ),别名为加速站点的域名。当⽤户向源站点发送请求时, DNS 服务器解析源站域名时会发现有 cname 记录,此时访问 CDN 专属的 DNS 服务器,并返回最佳接⼊点 IP ,⻚⾯访问对应 IP ,获取对应的资源。 优势:
- 解决了跨运营商和跨地域的问题,访问延迟⼤⼤降低。
- ⼤部分请求在 CDN 边缘节点完成,减轻了源站的压⼒。
TCP 的三次握手和四次挥手
三次握手建立连接
- 第一次握手:客户端将 TCP 报文标志位 SYN 置为 1,随机产生一个序号值 seq=x,保存在 TCP 首部的序列号(Sequence Number)字段里,指明连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入 SYN_SENT 状态,等待服务器端确认。
- 第二次握手:服务器端收到数据包后由标志位 SYN=1 知道客户端请求建立连接,服务器端将 TCP 报文标志位 SYN 和 ACK 都置为 1,ack=x+1,随机产生一个序号值 seq=y,并将该数据包发送给客户端以确认连接请求,服务器端进入 SYN_RCVD 状态。
- 第三次握手:客户端收到确认后,检查 ack 是否为 x+1,ACK 是否为 1 ,如果正确则将标志位 ACK 置为 1,ack=y+1,并将该数据包发送给服务器端,服务器端检查 ack 是否为 y+1,ACK 是否为 1 ,如果正确则连接建立成功,客户端和服务器端进入 ESTABLISHED 状态,完成三次握手,客户端与服务器端之间可以开始传输数据。
四次挥手断开连接
- 第一次挥手: 客户端将 FIN 标志位置为 1,设置随机序列号 seq=u,将数据包发送到服务端,客户端进入 FIN-WAIT-1 状态,这表示客户端没有数据要发送给服务端了。
- 第二次挥手:服务端收到 FIN=1 的数据包,将 ACK 标志位置为 1,将 ack 置为 u+1,将数据包发送给客户端,客户端进入 FIN-WAIT-2 状态,服务端进入 CLOSE-WAIT 状态,表示收到了关闭连接的请求。
- 第三次挥手: 服务端将 FIN 标志位置为 1,将设置随机序号值 seq=w,将数据包发送给客户端,请求关闭连接,同时服务端进入 LAST-ACK 状态。
- 第四次挥手: 客户端收到 FIN=1 数据包,将 ACK 标志位置为 1,将 ack 序号值置为 w+1,将数据包发送给服务端,客户端进入 TIME_WAIT 状态。服务端收到客户端的 ACK=1 数据包后,服务端进入 CLOSED 状态。此时,客户端等待 **2MSL(两个最大报文存活时间)**后没有收到回复,则证明服务端已正常关闭,客户端随即关闭连接。
为什么关闭连接是四次挥手
关闭连接时,当客户端发出 **FIN=1 **数据包时,只是表示客户端数据已经发送完毕。当服务端收到 FIN=1 数据包并返回 ACK=1 数据包,表示它已经知道客户端没有数据发送了,但是服务端还是可以发送数据到客户端,所以服务端不会立即关闭连接,直到服务端把数据也发送完毕。 当服务端也发送了 FIN=1 数据包时,就表示服务端也没有数据要发送了,之后彼此就会中断这次 TCP 连接。
TCP 和 UDP 区别有哪些
TCP 和 UDP 都属于 TCP/IP协议簇的一种,都属于传输层协议。
- TCP 是面向连接的,UDP 则是无连接的,即发送数据之前不需要建立连接
- TCP 提供的可靠的服务,TCP 连接传送的数据,无差错,不丢失,不重复,且按序到达,而 UDP 不保证可靠传输
- TCP 连接只能是一对一通信,UDP 支持一对一、一对多、多对一和多对多的通信
- TCP 面向字节流,UDP 是面向报文的
- TCP 首部开销较大,20 字节,UDP 只有 8 字节 | | TCP | UDP | | --- | --- | --- | | 是否连接 | 面向连接 | 面向无连接 | | 传输可靠性 | 可靠 | 不可靠 | | 通信方式 | 一对一通信 | 支持一对一、一对多、多对一、多对多通信 | | 传输方式 | 面向字节流 | 面向报文 | | 首部开销 | 大,20字节 | 小,8字节 | | 应用场景 | 可靠性高的应用 | 直播、视频会议、DNS 解析 | | 速度 | 慢 | 快 |
CSRF 跨站请求伪造
跨站请求伪造出现在利用 cookie-session作为身份认证的网站中,核心是利用了发送请求时会在请求头中自动携带 cookie的特性。诱导用户访问攻击者的网站,并发送请求给攻击者的网站。
完成一次 CSRF 需要两个条件:
- 登录网站 A,并生成
cookie - 在不登出 A 的情况下,访问网站 B
解决办法:
- 同源检测,检查请求头中的
referer属性 - 使用
token验证,加载页面下发token,提交数据时携带 token 做验证 - 双
cookie验证,提交数据时 JS 从cookie中获取值和表单一起提交 cookie, 设置SameSite: strict属性- 使用验证码,用户体验不友好
XSS 跨站脚本攻击
跨站脚本攻击是一种代码注入攻击,攻击者通过向目标网站注入恶意脚本,使之在浏览器运行。 根据攻击的来源,分为存储型、反射型、DOM 型:
| 攻击类别 | 攻击形式 | 攻击场景 |
|---|---|---|
| 存储型 | 后端数据库存储了恶意脚本,渲染到 HTML 时,执行恶意代码 | 评论、富文本 |
| 反射型 | url 上包含恶意代码,后端拼接 html 时直接使用了 url 上恶意代码 | 非单页面应用搜索,将 url 上搜索关键词插入到 html 中 |
| DOM型 | url 上包含恶意代码,前端直接获取了 url 上恶意代码执行 | 单页面应用搜索,将 url 上搜索关键词插入到 html 中 |
解决办法:
- 对输入的字符串进行格式校验
- 对 html 标签进行转义处理
- 对于富文本可以添加白名单,删除非法字符
- 设置 http-only,防止 cookie 被盗取
前端性能优化的方案有哪些
前端性能优化手段:减少请求数量,减小资源大小,优化网络连接,优化资源加载,减少回流重绘,构建优化。
- 减少请求数量
- 文件合并,并按需分配(公共库合并,其他页面组件按需分配)
- 图片处理:使用雪碧图,将图片转码 base64 内嵌到 HTML 中
- 减少重定向:尽量避免使用重定向,当页面发生了重定向,就会延迟整个 HTML 文档的传输
- 使用缓存:即利用浏览器的强缓存和协商缓存
- 减小资源大小
- 压缩:静态资源删除无效冗余代码并压缩
- webp:更小体积
- 开启 GZIP:HTTP 协议上的 GZIP 编码是一种用来改进WEB应用程序性能的技术
- 优化网络连接
- 使用 CDN
- 使用 DNS 预解析:DNS Prefetch,即 DNS 预解析就是根据浏览器定义的规则,提前解析之后可能会用到的域名,使解析结果缓存到
系统缓存中,缩短 DNS 解析时间,来提高网站的访问速度
- 优化资源加载
- 为优先级低的 script 脚本添加 defer 属性
- 减少回流重绘
- 构建优化:如 webpack 打包优化