前端面试题选题
# 前端面试题选题
p3 能基本答出来,
p4: 在 p3 基础上 能了解他们之间的差异,
p5:在 p4 基础上 能知道他们其中的一些原理及实现方案。
# js 面试题-10+3
# js 的数据类型有哪些
js基本类型数据都是直接按值存储在栈中的,包括 Undefined、Null、不是 new 出来的布尔、数字和字符串 。
js引用类型数据被存储于堆中, 如对象、数组、函数 。准确说,是引用类型数据的地址在栈中,但它指向的值在堆中。
此外,Symbol 和 BigInt 是 ES6 中新增的数据类型。
# 为什么 0.1+0.2!==0.3?
出现小数精度丢失的原因:计算机是通过二进制的方式存储数据的,所以计算机计算 0.1+0.2 的时候,实际上是计算的两个数的二进制的和。0.1 的二进制
是0.0001100110011001100...
(1100 循环),0.2 的二进制是:0.00110011001100...
(1100 循环),这两个数的二进制都是无限循环的数。
在 JavaScript 中只有一种数字类型:Number,它的实现遵循 IEEE 754 标准,使用 64 位固定长度来表示,也就是标准的 double 双精度浮点数。在二进制科学表示法中,双精度浮 点数的小数部分最多只能保留 52 位,再加上前面的 1,其实就是保留 53 位有效数字,剩余的需要舍去,遵从“0 舍 1 入”的原则。
根据这个原则,0.1 和 0.2 的二进制数相加,再转化为十进制数就是:0.30000000000000004
。此时就出现了精度丢失。而大数字(大于 2^53)也有精度问题。
避免精度丢失的方法:
- 使用第三方类库例如 math.js 或 decimal.js 等
- 先放大成整数,再运算,最后缩小成小数。
扩展:对于大数字的运算呢?转为字符串运算。
# 如何遍历伪数组?
arguments
是一个对象,它的属性是从 0 开始依次递增的数字,还有callee
和length
等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach
, reduce
等,所
以叫它们类数组。
要转化类数组,有三个方法:
(1)将数组的方法应用到类数组上,这时候就可以使用call
和apply
方法,如:
function foo() {
Array.prototype.forEach.call(arguments, (a) => console.log(a))
}
2
3
(2)使用 Array.from 方法将类数组转化成数组:
function foo() {
const arrArgs = Array.from(arguments)
arrArgs.forEach((a) => console.log(a))
}
2
3
4
(3)使用展开运算符将类数组转化成数组
function foo() {
const arrArgs = [...arguments]
arrArgs.forEach((a) => console.log(a))
}
2
3
4
还可以直接将 arguments 使用扩展运算符展开
function foo(...args) {
for (let item of args) {
console.log(item)
}
}
2
3
4
5
甚至不需要额外处理,直接使用 for...of 遍历
function foo() {
for (let item of arguments) {
console.log(item)
}
}
2
3
4
5
# 对 this 对象的理解
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数 组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
# 对闭包的理解
闭包是指引用了访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途;
闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私 有变量。
闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。
function A() {
let a = 1
window.B = function() {
console.log(a)
}
}
A()
B() // 1
2
3
4
5
6
7
8
9
在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
2
3
4
5
首先因为 setTimeout
是个异步函数,所以会先把循环全部执行完毕,这时候 i
就是 6 了,所以会输出一堆 6。解决办法有三种:
- 第一种是使用闭包的方式
for (var i = 1; i <= 5; i++) {
;(function(j) {
setTimeout(() => {
console.log(j)
}, j * 1000)
})(i)
}
2
3
4
5
6
7
在上述代码中,首先使用了立即执行函数将 i
传入函数内部,这个时候值就被固定在了参数 j
上面不会改变,当下次执行 timer
这个闭包的时候,就可以使用外部函数的变
量 j
,从而达到目的。
- 第二种就是使用
setTimeout
的第三个参数,这个参数会被当成timer
函数的参数传入。
for (var i = 1; i <= 5; i++) {
setTimeout(
(j) => {
console.log(j)
},
i * 1000,
i
)
}
2
3
4
5
6
7
8
9
- 第三种就是使用
let
定义i
了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
setTimeout(() => {
console.log(i)
}, i * 1000)
}
2
3
4
5
# 箭头函数与普通函数的区别
- 更加简洁。
let fn = () => void doesNotReturn();
- 没有自己的 this。它只会在自己作用域的上一层继承 this,也可以说成是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值。
- call**()、**apply()、bind()等方法不能改变箭头函数中 this 的指向
- 箭头函数不能作为构造函数使用。如果 new 一个箭头函数会抛出错误。
- 箭头函数没有自己的 arguments
- 箭头函数没有 prototype
- 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
# 你是如何理解 Proxy 的
let p = new Proxy(target, handler)
target
代表需要添加代理的对象,handler
用来自定义对象中的操作,比如可以用来自定义 set
或者 get
函数。
下面来通过 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, property)
return Reflect.set(target, property, value)
},
}
return new Proxy(obj, handler)
}
let obj = { a: 1 }
let p = onWatch(
obj,
(v, property) => {
console.log(`监听到属性${property}改变为${v}`)
},
(target, property) => {
console.log(`'${property}' = ${target[property]}`)
}
)
p.a = 2 // 监听到属性a改变
p.a // 'a' = 2
复制代码
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
在上述代码中,通过自定义 set
和 get
函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
当然这是简单版的响应式实现,如果需要实现一个 Vue 中的响应式,需要在 get
中收集依赖,在 set
派发更新,之所以 Vue3.0 要使用 Proxy
替换原本的 API 原因在于
Proxy
无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy
可以完美监听到任何方式的
数据改变,唯一缺陷就是浏览器的兼容性不好。
# 对原型、原型链的理解
构造函数在被创建出来的时候,系统会默认的帮构造函数创建并且关联一个空对象,这个空对象就是原型。原型中的成员(属性和方法),可以被和这个原型相关的构造函数创建出来 的所有的对象(实例)所共享。
可以通过 2 种方式访问原型
构造函数.prototype
对象.__proto__
一般浏览器中都实现了 __proto__ 属性来访问原型,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过 这个方法来获取对象的原型。
原型中默认会有一个属性 constructor,指向和原型相关的构造函数。在给构造函数.prototype 重新赋值的时候,这个 constructor 属性就没有了,除非手动添加。
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型 链。从语言的层面,原型也是对象,所以原型也有原型,这样就构成原型链。
# 手写一个 new 操作过程
// 模拟new一个对象的过程
/**
* 1.创建一个空对象,继承构造函数的prototype对象
* 2.执行构造函数,传入相关参数,并将this指向第一步的空对象
* 3.如果构造函数返回了对象,则返回该对象,否则返回第一步的对象
*/
function newFn1(constructorFn, ...arg) {
// 第1种
// const obj = {}
// Object.setPrototypeOf(obj, constructorFn.prototype)
// 第2种
// const obj = new Object()
// obj.__proto__ = constructorFn.prototype
// 第3种
const obj = Object.create(constructorFn.prototype)
const resObj = constructorFn.apply(obj, arg)
return resObj instanceof constructorFn ? resObj : obj
}
function Person(n) {
console.log('person')
this.x = n
this.say = function() {
console.log(this.x)
}
}
const p = newFn1(Person, 2)
p.say()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 宏任务和微任务分别有哪些,优先级如何
宏任务
setTimeout
setInterval
setImmediate
微任务
Promise.then
process.nextTick(node 中)
MutationObserver
微任务>宏任务
# commonJs 和 es6 模块化的区别是什么(+)
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块 就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readfile } = require('fs')
// 等同于
let _fs = require('fs')
let stat = _fs.stat
let exists = _fs.exists
let readfile = _fs.readfile
2
3
4
5
6
7
8
上面代码的实质是整体加载fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时
才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。
// ES6模块
import { stat, exists, readFile } from 'fs'
2
上面代码的实质是从fs
模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加
载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
CommonJs | ES6 | |
---|---|---|
加载模式 | 运行时加载 | 编译时加载 |
模块是否对象 | 是 | 否 |
加载效率 | 低 | 高 |
采用严格模式 | 否 | 自动采用 |
欢迎补充.. |
# 实现继承的几种方式以及他们的优缺点(+)
# 1.原型链继承
利用Sub.prototype = new Super()
,这样连通了子类-子类原型-父类。
function Super() {
this.flag = true
}
Super.prototype.getFlag = function() {
return this.flag
}
function Sub() {
this.subFlag = true
}
// 实现继承
Sub.prototype = new Super()
// 添加方法,需要在继承之后
Sub.prototype.getSubFlag = function() {
return this.subFlag
}
const sub = new Sub()
const subFlag = sub.getSubFlag()
console.log(subFlag)
const superFlag = sub.getFlag()
console.log(superFlag)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
优点:
- 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
- 父类新增原型方法/原型属性,子类都能访问到
- 简单,易于实现
缺点:
- 要想为子类新增属性和方法,必须要在继承语句之后执行,不能放到构造器中
- 无法实现多继承
- 来自原型对象的引用属性是所有实例共享的
- 创建子类实例时,无法向父类构造函数传参
# 2.构造函数继承
在构造子类构造函数时内部使用call
或apply
来调用父类的构造函数
function Super() {
this.flag = true
}
function Sub() {
//如果父类可以需要接收参数,这里也可以直接传递
Super.call(this)
}
const obj = new Sub()
obj.flag = false
const obj2 = new Sub()
console.log(obj2.flag) //依然是true,不会相互影响
2
3
4
5
6
7
8
9
10
11
12
13
14
优点:
- 解决了原型链继承中,子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(
call
绑定多个父类对象)
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
# 3.组合继承
使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承
function Super() {
this.flag = true
}
Super.prototype.getFlag = function() {
return this.flag
}
function Sub() {
this.subFlag = true
// 继承实例属性,第一次调用Super
Super.call(this)
}
//继承父类方法,第二次调用Super
Sub.prototype = new Super()
// 注意,这里Sub.prototype.constructor指向了Super,不符合constructor的定义。因此要加上一行
Sub.prototype.constructor = Sub
Sub.prototype.getSubFlag = function() {
return this.subFlag
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
优点:
- 弥补了原型链继承和构造函数继承的缺点,既可以继承实例的属性和方法,也可以继承原型的属性和方法
instanceof
和isPrototypeOf( )
也能用于识别基于组合继承创建的对象
缺点:
- 调用了两次父类构造函数, 造成了不必要的消耗
# 4.寄生组合继承
寄生组合式继承就是为了降低调用父类构造函数的开销而出现的,不必为了指定子类型的原型而调用超类型的构造函数
function extend(subClass, superClass) {
// 复制父类的原型
const prototype = Object.create(superClass.prototype)
// 将复制的原型的constructor指向子类
prototype.constructor = subClass
// 将子类的prototype指向复制的原型
subClass.prototype = prototype
//指定对象
}
function Super() {
this.flag = true
}
Super.prototype.getFlag = function() {
return this.flag
}
function Sub() {
this.subFlag = true // 继承实例属性,第一次调用Super
Super.call(this)
}
extend(Sub, Super)
Sub.prototype.getSubFlag = function() {
return this.subFlag
}
const sub = new Sub()
console.log(sub.getSubFlag()) // true
console.log(sub.getFlag()) // true
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
优点:
- 弥补了组合继承调用两次父类构造函数的缺陷
# 5.Class 继承
class Sub extends Super{}
class 本质上就是原型继承的语法糖,比 ES5 的通过手动修改原型链实现继承,要清晰和方便很多。
# 描述 EventLoop(事件循环)过程(+)
EventLoop 实现过程:
- 所有同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,有一个任务队列(Task Queue),只要异步任务有了运行结果,就在任务队列中进行等待。
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,那些对应的异步任务就结束等待状态,进入执行栈,开始执行。
- 当前任务执行完毕,会查看任务队列中是否还有异步回调在等待,如果有,则继续执行。
- 这个过程不断循环,所以就是事件循环。
任务队列可分为宏任务和微任务,任务执行优先级:同步任务>微任务>宏任务。
# css 面试题-2
# 如何实现水平垂直居中?
- 弹性布局
- absolute 布局
- ...
# 移动端屏幕各式各样,如何适配?
- rem 布局,什么是 rem,如何使用 rem 做移动端适配
- vw,vh 布局
- @media screen 媒体查询做 pc 和移动端适配
# css中flex:1;
是哪些规则的集合
flex-grow: 1;
flex-shrink: 1;
flex-basis: 0%;
2
3
# Vue 面试题-10+2
# Vue 响应式原理
Vue 采用属性劫持和发布订阅模式结合的方式,劫持各个属性的 setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调,更新视图。主要包括几个部分:
- Observer。对需要渲染的数据对象进行递归遍历,使用 Object.defineProperty() (Vue3 是 Proxy)来劫持其 setter 和 getter。当使用模板进行初始化渲染时,会访问到对象 (实例属性),从而触发 getter,此时实例化 Watcher,并添加到订阅器(dep);当程序修改实例属性,会触发 setter,此时订阅器会调用 notify 通知 Watcher,调用 update,从而更新视图。
- Compiler。解析模板,将模板转为真实 DOM,同时解析 v-指令和表达式,对模板中的变量进行初始化渲染,同时实例化 Watcher 并传入更新回调,如果有事件绑定则监听元素对应 事件。
- Watcher。订阅者,是 Observer 和 Compiler 之间的桥梁,调用自身的 update 方法后,可以触发更新回调,从而更新对应 DOM 节点的视图。
# v-if 和 v-show 的区别
- v-if 是惰性渲染,当表达式为
false
时,组件渲染时不会渲染该节点,只有表达式为true
时,才会更新渲染。切换过程中销毁和重建内部的事件监听和子组件。 - v-show 在渲染组件时总会渲染该节点,它是通过 css 设置 display 属性来控制 DOM 的显示隐藏,如果涉及到频繁的显示隐藏切换,v-show 比 v-if 更合适。
# data 为什么是一个函数而不是对象
如果是一个对象,由于组件可以被复用,那么当一个组件被使用了多次,并且组件的实例属性被修改后,因为 js 中对象是按引用传递,那么一个组件的实例属性被修改后,另一个组 件的该属性也会改变,导致状态互相污染。实际上,在开发模式下,如果将 data 写成对象,是会编译报错的。
而写成函数,并返回一个字面量对象的方式,则保证了,每次组件实例化时,都有各自私有的实例属性备份,不会污染其他组件的状态。
# 使用过$nextTick 吗?它的原理是什么?(p3)
它的本质是 Vue 对 EventLoop 的一种应用。原理是利用 js 的 Promise,MutationObserver 和 setTimeout 等方法来实现 Vue 内部的异步回调队列。
相比原生 dom 渲染,vue 的异步队列更新机制性能更好,表现为:
- 如果频繁更改实例属性,采用异步更新的方式会将多个更改操作进行合并,减少了 dom 的无用渲染。
- 由于 vue 引入了虚拟 DOM,如果同步更新,那么在 dom 重新绘制过程中也会涉及到更多计算,导致性能降低。
使 用$nextTick的场景是,当实例属性更新后,相关的dom即将进行更新。而如果立即获取dom的状态,这时由于视图没有立即更新,导致获取的dom状态不是最新的,此时可以通过vm.$nextTick(fn) 的方式来获取更新后的 dom 节点。
关联问题:Vue data 中某一个属性的值发生改变后,视图会立即同步执行重新渲染吗?
不会,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化, Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推 入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。
# Vue 子组件和父组件执行顺序
加载渲染过程:
- 父组件 beforeCreate
- 父组件 created
- 父组件 beforeMount
- 子组件 beforeCreate
- 子组件 created
- 子组件 beforeMount
- 子组件 mounted
- 父组件 mounted
更新过程:
- 父组件 beforeUpdate
- 子组件 beforeUpdate
- 子组件 updated
- 父组件 updated
销毁过程:
- 父组件 beforeDestroy
- 子组件 beforeDestroy
- 子组件 destroyed
- 父组件 destoryed
# 常见的组件通信方式有哪些
- props/$emit。数据是单向流动的,父组件通过
props
向子组件传递数据,子组件通过$emit
向父组件通信。 - eventBus($emit/$on)。适用于父子组件、非父子组件。通过实例化 Vue 生成事件总线,组件通过发送事件和监听事件来通信。使用方便,但不利于维护。
- 依赖注入(provide/inject)。适用于父子、祖孙组件。provide 和 inject 是两个钩子,分别用来发送数据和接收数据。注意,依赖注入所提供的属性是非响应式的。
- ref/$refs。适用于父组件访问子组件实例的数据和方法。
- $parent/$children。其中$parent可以让子组件访问父组件的实例,而$children 可以访问到子组件,它是一个数组,不能保证顺序。
- $attrs/$listeners。使用
$attrs/$listeners
可以优雅地实现组件之间的多属性数据通信。配合v-bind="$attrs"
可以轻松实现跨代通信。$attrs
:继承所有的父组件属性(除了 prop 传递的属性、class 和 style ),一般用在子组件的子元素上$listeners
:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相 当于子组件继承父组件的事件)
- vuex。数据状态管理模式,将公共的数据抽离出来,统一通过提交的方式进行修改,数据流动清晰,便于追踪。
# Vue 路由模式有哪些,原理是什么
Vue-Router 有两种模式:hash 模式和history 模式
hash 模式
比如
https://abc.com/#/home
,这里#/home
就是 hash 值。hash 值的改变不会重新加载页面。
主要原理是:window 监听 onhashchange()事件,根据变化的 hash 值加载对应的 vue 组件,这个过程不涉及到服务端请求,整个过程在浏览器完成。
history 模式
比如
https://abc.com/home
,它区别于 hash 模式的地方在于没有#
,是利用 History API 来实现前端路由。具体是利用pushState()
和replaceState()
方法修改 url,同时加载 vue 组件。可能的风险点在于刷新页面或者直接输入 url 时,会返回 404,需要服务端进行配置:如果 URL 匹配不到任何静态资源,则应该返回同一个
index.html
页面。
# 虚拟 DOM 的解析过程
- js 对象表示 DOM。首先对将要插入到文档中的 DOM 树结构进行分析,使用 js 对象将其表示出来,比如一个元素对象,包含 TagName、props 和 Children 这些属性。然后将 这个 js 对象树给保存下来,最后再将 DOM 片段插入到文档中。
- diff 算法比较差异。当页面的状态发生改变,需要对页面的 DOM 的结构进行调整的时候,首先根据变更的状态,重新构建起一棵对象树,然后将这棵新的对象树和旧的对象树 进行比较,记录下两棵树的的差异。
- **应用差异更新视图。**最后将记录的有差异的地方应用到真正的 DOM 树中去,这样视图就更新了。
# 如何理解 Vuex(p3)
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样可以方便地跟踪每一个状态的变化。
- vue Components 会触发(dispatch)一些事件或动作,也就是 Actions;
- 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
- 然后 Mutations 就去改变(Mutate)State 中的数据;
- 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。
# Vue3.0 有什么更新(p3)
- 基于 Proxy 实现响应式,而 vue2 是通过 ES5 的 Object.defineProperty 来实现。避免了对数组响应式更新做的 hack 和 vm.$set 的方式。
- 组合式 API 的使用。
- 通过 setup 选项,将统一逻辑关注点的代码集中在一起,便于维护。
- 生命周期钩子和其他如 ref 等方法都可以按需引入。
- 没有 this。
- 模板不需要最外层的节点,即 template 下一级可以有多个元素或组件。
- 移除 filter。
- Teleport。可以将组件插入到任意 dom 节点。
- Suspense。利用作用域插槽,可以方便显示异步组件加载前后的视图
- 加强对 ts 的支持。
# vue tempalte render 的过程(+)
vue 的模版编译过程主要如下:template -> ast -> render 函数->生成虚拟 DOM。简述如下:
在 beforeMount 之前执行编译过程,第一步通过 html-parser 将 template 解析成 ast 抽象语法树,第二步通过 optimize 优化 ast 并标记静态节点和静态根节点,第三步通过 generate 将 ast 抽象语法树编译成 render 字符串并将静态部分放到 staticRenderFns 中,最后通过 new Function(render)生成 render 函数。在 beforeMount 和 mounted 之 间执行 render 函数生成 VNode,然后通过 patch(VNode)生成 dom 树并挂载,调用 mounted。 https://blog.csdn.net/lyt_angularjs/article/details/105250391
template->ast
目标:把 tamplate 转换为 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。
解析过程:利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的 回调函数,来达到构造 AST 树的目的。
对静态节点做优化
optimize(ast, options)
1深度遍历 AST,分析出哪些是静态节点,给其打一个标记,为后续更新渲染可以直接跳过静态节点做优化。
生成 render 函数
const code = generate(ast, options)
1将 AST 编译成 render 字符串,再使用 new Function()生成 render 函数。
生成 render 函数后,之后实例进行挂载和节点更新,都会调用 render 进行渲染。
生成虚拟 DOM
Vue 实例挂载根节点(#app)或者在 beforeMount 之后,开始调用实例上的 render 函数,递归生成 VNode(就是虚拟 DOM)
# DIFF 算法的原理(+)
分为 3 步
- 使用深度遍历标记新旧 DOM 树的节点,保证比较的时候比较出差异类型
- 通过同层对比,比较差异。差异有几种类型,如节点类型改变,属性改变,文本改变,移动新增删除类型。同层保证了 O(n)的时间复杂度。
- 将差异应用到真实 DOM。深度遍历真实 DOM,如果有差异,将该差异更新到真实 DOM 上。
# 网络协议-4+1
# 当在浏览器输入一段 url,回车后发生了什么?
**URL 解析。**浏览器解析 URL 为协议,域名,端口,文件目录,文件名,参数和锚;同时检查 URL 是否出现非法字符,有则进行转义。
**缓存判断。**检查资源是否存在缓存中,如果有且未失效,则直接使用,否则向服务器发起请求。
**DNS 解析。**将域名解析为 IP 的过程,依次请求如下,如果有缓存就直接返回,否则继续查询
- 本地(hosts 文件)是否有域名-IP 的缓存
- 向本地 DNS 服务器查询(LDNS,一般是网络服务提供商,比如公司,学校,电信联通等,大多数域名解析在这里完成)
- LDNS 向根域名服务器查询,获取顶级域名服务器的地址
- LDNS 向顶级域名服务器查询,获取权威域名服务器的地址
- LDNS 向权威域名服务器查询,获得最终的 IP 地址
**获取 MAC 地址。**拿到 IP 还不够,还需要知道目的主机的 MAC 地址。可以使用 ARP 协议来获取目的主机的 MAC 地址。
TCP 三次握手。
- 客户端到服务端,发送 SYN + seq(x)
- 服务端到客户端,发送 ACK + seq(y) + ack(x+1)
- 客户端到服务端,发送 ACK + seq(x+1) + ack(y+1)
**HTTPS 握手。**对于 HTTPS 协议,还需要一个 TLS 的四次握手
- 客户端到服务端,发送协议版本号 + 一个随机数 + 供选择的加密方法。
- 服务端到客户端,发送一个随机数 + 数字证书。
- 客户端检查数字证书有效,使用证书的公钥对随机数加密,发送 该加密的随机数。
- 服务端使用私钥解密得到秘钥,发送前面内容的 hash 给客户端检验,连接建立。
- 之后双方使用这 3 个随机数生成的秘钥,对报文加密后通信。
服务端处理请求,将数据返回给客户端。
**html 解析。**客户端接收到 html 后,开始从上到下解析。如果遇到外部 script 和外部 css,则另外发起请求。浏览器根据 html 标签生成 DOM 树,根据 css 生成 CSSOM 树,最后将 2 颗树合并为 1 颗渲染树。
**页面渲染。**浏览器根据渲染树进行布局(位置大小)和绘制(像素点绘制)。如果有样式覆盖或 js 修改 DOM,则会进行回流和重绘。
DNS 服务器包括:根域名服务器(全球共 13 台),顶级域名服务器(如.com),权威域名服务器(如 baidu.com)。
# 什么是 HTTPS?它与 HTTP 的区别是什么?
HTTPS 在 HTTP 和 TCP 之间多了一层 SSL/TLS(安全套接层),提供身份验证、信息加密和完整性校验的功能。职责是对发送数据加密,对接收数据解密。
区别:
HTTP | HTTPS | |
---|---|---|
安全性 | 明文传输 | SSL 加密,更安全 |
端口 | 80 | 443 |
是否需要 CA 证书 | 不需要 | 需要 |
# 谈谈对 http 缓存的理解
包括强缓存和协商缓存,简单步骤为:
1.浏览器检查该资源的 http 头部信息
2.如果存在 cache-control 或 expires 即命中强缓存,再检查是否过期,未过期则直接读取本地资源。
3.如果没有命中强缓存,浏览器会发请求到服务器,由服务器判断本地缓存是否有效(有无修改),标识是响应头的 last-modified 或 etag 字段,如果存在且与对应字段相等,则 服务器返回 304,浏览器将从本地读取。
注:
If-None-Match 和 Etag 相等 或 If-Modified-Since 和 Last-Modified 时间一致
知道 http2 吗?有什么特点?
相比 HTTP1.1,HTTP2.0 有如下变化:
- **二进制协议。**HTTP1.1 报文头是文本,报文体是文本或二进制。HTTP2 全是二进制协议,分为头信息帧和数据帧。使用二进制解析效率高,没有冗余字段,提高了性能,减少了 带宽。
- **多路复用。**HTTP2 中,复用同一个 TCP 连接,客户端和服务端都可以同时发送多个请求和响应,解决了“队头堵塞”问题。
- **数据流。**HTTP2 才有的概念,建立的连接可以承载任意数量的双向数据流,他们是没有顺序的,可能属于不同的请求。每个请求和响应属于一个数据流,具有唯一的编号标记。
- **头信息压缩。**HTTP1.1 中,请求头如 Cookie 和 User Agent 是一样的,但每次请求都会携带,有一定性能损耗。HTTP2 做了优化,第一,头信息使用压缩算法压缩后发送;第 二,客户端和服务端同时维护一张头信息表,使用索引号标识,只发送索引号就可以了。
- **服务端推送。**HTTP2 允许服务器主动向客户端发送静态资源,并缓存在客户端,减小请求延迟。
# HTTPS 是如何保证数据安全的?(+)
对称加密与非对称加密结合
利用非对称加密的高安全和对称加密的加解密速度。将对称加密的密钥使⽤⾮对称加密的公钥进⾏加密,然后发送出去,接收⽅使⽤私钥进⾏解密得到对称加密的密钥,然后双⽅可 以使⽤对称加密来进⾏沟通。
验证数字签名
假设证书颁发机构(CA)颁发了数字证书,包括:签发者、证书⽤途、使⽤者公钥、使⽤者私钥、使⽤者的 HASH 算法、证书到期时间等。虽然能表明身份,但不能证明,因为中间 人可以篡改证书。
这时用到新的技术:数字签名。
数字签名就是⽤ CA ⾃带的 HASH 算法对证书的内容进⾏ HASH 得到⼀个摘要,再⽤ CA 的私钥加密,最终组成数字签名。当别⼈把他的证书发过来的时候,我再⽤同样的 Hash 算 法,再次⽣成消息摘要,然后⽤ CA 的公钥对数字签名解密,得到 CA 创建的消息摘要,两者⼀⽐,就知道中间有没有被⼈篡改了。这个时候就能最⼤程度保证通信的安全了。
# 性能优化-2
# 如何提高页面性能?
从这几个方面做考虑:
资源 合并资源(将 css,js,雪碧图等合并) 减小入口文件体积,比如路由懒加载,组件动态加载 压缩文件大小,图片压缩,gzip压缩 使用webp 预加载 第三方库/组件按需加载
缓存 静态资源缓存策略
请求 SSR 服务端合并前端请求 独立的图片服务器,防止 http 阻塞 CDN 加速
# 如何提⾼webpack的构建速度?
- 多⼊⼝情况下,使⽤ CommonsChunkPlugin 来提取公共代码
- 通过 externals 配置来提取常⽤库
- 利⽤ DllPlugin 和 DllReferencePlugin 预编译资源模块 通过 DllPlugin 来对那些我们引⽤但是绝对不会修改的 npm 包来进⾏预编译,再通过 DllReferencePlugin 将预编译 的模块加载进来。
- 使⽤ Happypack 实现多线程加速编译
- 使⽤ webpack-uglify-parallel 来提升 uglifyPlugin 的压缩速度。 原理上 webpack-uglify-parallel 采⽤了多核并⾏压缩来提升压缩速度
- 使⽤ Tree-shaking 和 Scope Hoisting 来剔除多余代码
# 前端安全-1
# 什么是 XSS 攻击?如何防范?
XSS 攻击指的是跨站脚本攻击,是一种代码注入攻击。攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而获取用户的信息如 cookie 等。
攻击者可以用过这种攻击方式进行以下操作:
获取页面的数据,如 DOM、cookie、localStorage
DOS 攻击,发送合理请求,占用服务器资源,从而使正常有用户无法访问服务器
破坏页面结构
浏览劫持(将链接指向其他网站)
攻击类型
XSS 可以分为存储型、反射型和 DOM 型。
存储型指的是恶意脚本会存储在目标服务器上,当浏览器请求数据时,脚本从服务器传回并执行。
反射型指的是攻击者诱导用户访问一个带有恶意代码的 URL 后,服务器端接收数据后处理,然后把带有恶意代码的数据发送到浏览器端,浏览器端解析这段带有 XSS 代码的数据后 当做脚本执行,最终完成 XSS 攻击。
DOM 型指的通过修改页面的 DOM 节点形成的 XSS。
如何防御 XSS 攻击
- 转义
- 使用 CSP ,CSP 的本质是建立一个白名单,告诉浏览器哪些外部资源可以加载和执行,从而防止恶意代码的注入攻击。
通常有两种方式来开启 CSP,一种是设置 HTTP 首部中的 Content-Security-Policy,一种是设置 meta 标签的方式
- 对一些敏感信息进行保护,比如 cookie 使用 http-only,使得脚本无法获取。也可以使用验证码,避免脚本伪装成用户执行一些操作。
# 什么是 CSRF 攻击?
CSRF 攻击指的是跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求。
CSRF 攻击的本质是利用 cookie 会在同源请求中携带发送给服务器的特点,以此来实现用户的冒充。
常见的 CSRF 攻击有三种:
GET 类型的 CSRF 攻击,比如在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
POST 类型的 CSRF 攻击,比如构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
链接类型的 CSRF 攻击,比如在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。