金三银四,来一波面试总结。持续更新。。。
javascript
解决90%的大厂基础面试题
js中的数据类型
JavaScript 中共有七种内置数据类型,包括基本类型和对象类型。
1. 基本类型
- string(字符串)
- boolean(布尔值)
- number(数字)
- symbol(符号)
- null(空值)
- undefined(未定义) 注意:
- string 、number 、boolean 和 null undefined 这五种类型统称为原始类型(Primitive),表示不能再细分下去的基本类型
- symbol是ES6中新增的数据类型,symbol 表示独一无二的值,通过 Symbol 函数调用生成,由于生成的 symbol 值为原始类型,所以 Symbol 函数不能使用 new 调用;
- null 和 undefined 通常被认为是特殊值,这两种类型的值唯一,就是其本身。
2. 对象类型
对象类型也叫引用类型,array和function是对象的子类型。对象在逻辑上是属性的无序集合,是存放各种值的容器。对象值存储的是引用地址,所以和基本类型值不可变的特性不同,对象值是可变的。
数组的常用方法
1. 改变原数组的方法
- splice() 添加/删除数组元素
语法:arrayObject.splice(index,howmany,item1,.....,itemX)
参数:
1.index:必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
2.howmany:可选。要删除的项目数量。如果设置为 0,则不会删除项目。
3.item1, ..., itemX:可选。向数组添加的新项目。
返回值: 如果有元素被删除,返回包含被删除项目的新数组。
- sort() 数组排序
语法:arrayObject.sort(sortby)
参数:
1.sortby可选。规定排序顺序。必须是函数。。
返回值: 返回包排序后的新数组。
- pop() 删除一个数组中的最后的一个元素
语法:arrayObject.pop()
参数:无
返回值: 返回被删除的元素。
- shift() 删除数组的第一个元素
语法:arrayObject.shift()
参数:无
返回值: 返回被删除的元素。
- push() 向数组的末尾添加元素
语法:arrayObject.push(newelement1,newelement2,....,newelementX)
参数:
1.newelement1必需。要添加到数组的第一个元素。
2.newelement2可选。要添加到数组的第二个元素。
3.newelementX可选。可添加若干个元素。
返回值: 返回被删除的元素。
- unshift() 向数组的开头添加一个或更多元素
语法:arrayObject.unshift(newelement1,newelement2,....,newelementX)
参数:
1.newelement1必需。要添加到数组的第一个元素。
2.newelement2可选。要添加到数组的第二个元素。
3.newelementX可选。可添加若干个元素。
返回值: arrayObject 的新长度。。
- reverse() 颠倒数组中元素的顺序
语法:arrayObject.reverse()
参数:无
返回值: 颠倒后的新数组。
- copyWithin() 指定位置的成员复制到其他位置
语法:array.copyWithin(target, start = 0, end = this.length)
参数:
1.target(必需):从该位置开始替换数据。如果为负值,表示倒数。
2.start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
3.end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
返回值: 返回当前数组。
- fill() 填充数组
语法:array.fill(value, start, end)
参数:
1.value必需。填充的值。
2.start可选。开始填充位置。
3.end可选。停止填充位置 (默认为 array.length)
返回值: 返回当前数组。
2. 不改变原数组的方法
- slice() 浅拷贝数组的元素
语法:array.slice(begin, end);
参数:
1.begin(可选): 索引数值,接受负值,从该索引处开始提取原数组中的元素,默认值为0。
2.end(可选):索引数值(不包括),接受负值,在该索引处前结束提取原数组元素,默认值为数组末尾(包括最后一个元素)。
返回值: 返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象,且原数组不会被修改。
- join() 数组转字符串
语法:array.join(str)
参数:
1.str(可选): 指定要使用的分隔符,默认使用逗号作为分隔符。
返回值: 返回生成的字符串。
- concat() 合并两个或多个数组
语法:var newArr =oldArray.concat(arrayX,arrayX,......,arrayX)
参数:
1.arrayX(必须):该参数可以是具体的值,也可以是数组对象。可以是任意多个。
返回值: 返回返回合并后的新数组。
- indexOf() 查找数组是否存在某个元素
语法:array.indexOf(searchElement,fromIndex)
参数:
1.searchElement(必须):被查找的元素
2.fromIndex(可选):开始查找的位置(不能大于等于数组的长度,返回-1),接受负值,默认值为0。
返回值: 返回下标
- lastIndexOf() 查找指定元素在数组中的最后一个位置
语法:arr.lastIndexOf(searchElement,fromIndex)
参数:
1.searchElement(必须): 被查找的元素
2.fromIndex(可选): 逆向查找开始位置,默认值数组的长度-1,即查找整个数组。
返回值: 方法返回指定元素,在数组中的最后一个的索引,如果不存在则返回 -1。(从数组后面往前查找)
- includes() 查找数组是否包含某个元素
语法:array.includes(searchElement,fromIndex=0)
参数:
1.searchElement(必须):被查找的元素
2.fromIndex(可选):默认值为0,参数表示搜索的起始位置,接受负值。正值超过数组长度,数组不会被搜索,返回false。负值绝对值超过长数组度,重置从0开始搜索。
返回值: 返回布尔
ES5、Es6数组方法
数组去重
合并两个数组的方法
concat、for 循环、扩展运算法、push.apply 这些方法都可以
- concat 连接两个或更多的数组,并返回结果
var a = [1,2,3];
var b = [4,5,6];
var c = a.concat(b); //c=[1,2,3,4,5,6];
// 如果是数据量比较小的时候,还可以勉强用,如果数据量大的时候,这个就不妥了
- for循环 遍历其中一个数组,把该数组中的所有元素依次添加到另外一个数组中
for( var i in b)
{
a.push(b[i]);
}
// 可以解决第一种方案中对内存的浪费,但是会有另一个问题:丑!这么说不是没有道理,如果能只用一行代码就搞定
- apply func.apply(obj,argv),argv是一个数组。所以我们可以利用这点,直上代码:
a.push.apply(a,b)
调用a.push这个函数实例的apply方法,同时把,b当作参数传入,这样a.push这个方法就会遍历b数组的所有元素,达到合并的效果。
可以把b看成[4,5,6],变成这样:a.push.apply(a,[4,5,6]); 等同于 a.push(4,5,6);
- 扩展运算法
a.push(...b) 或者 const c = [...a,...b]
合并两个对象
Object.assign、扩展运算法、bject.keys()、手写深浅拷贝 都可以
- Obj.assign() 可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
Object.assign(target, ...sources)
1. 复制一个对象 const obj = { a: 1 ,b:2};
const copyObj = Object.assign({}, obj);
console.log(copyObj); // { a: 1,b:2 }
2. 合并多个对象
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };
const obj = Object.assign(o1, o2, o3);
console.log(obj); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, 且目标对象自身也会改变。
- 扩展运算法
const obj1 = { a: 1 };
const obj2 = { b: 2 };
let obj3={...obj1,...obj2};
- Object.keys()
Object.keys(obj1).forEach(key => {
obj2[key] = obj1[key]
})
深拷贝、浅拷贝
浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;
区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制;
- 浅拷贝的实现方式
- 自定义函数
function simpleCopy (initalObj) {
var obj = {};
for ( var i in initalObj) {
obj[i] = initalObj[i];
}
return obj;
}
- ES6 的 Object.assign()
let newObj = Object.assign({}, obj);
- ES6 的对象扩展
let newObj = {...obj};
- 深拷贝的实现方式
- JSON.stringify 和 JSON.parse
用 JSON.stringify 把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象。
let newObj = JSON.parse(JSON.stringify(obj));
- lodash
用 lodash 函数库提供的 _.cloneDeep 方法实现深拷贝。
var _ = require('lodash')
var newObj = _.cloneDeep(obj)
- 自己封装
function deepClone(obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === "object") {
// for...in 会把继承的属性一起遍历
for (let key in obj) {
// 判断是不是自有属性,而不是继承属性
if (obj.hasOwnProperty(key)) {
//判断ojb子元素是否为对象或数组,如果是,递归复制
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = this.deepClone(obj[key]);
} else {
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
如何实现正则的深拷贝
获取精度更高的时间
浏览器使用 performance.now() 可以获取到 performance.timing.navigationStart 到当前时间之间的微秒数
Node.js 使用 process.hrtime 返回一个数组,其中第一个元素的时间以秒为单位,第二个元素为剩余的纳秒
对象的键支持什么类型?
很多人都会认为对象的键是字符串类型,如果在以前确实没错,但是ES2015版本中对象的键类型还可以是Symbol。
const person = {
name: 'yd',
[Symbol()]: 18
}
for in 和 for of
- for in
- 1.一般用于遍历对象的可枚举属性。以及对象从构造函数原型中继承的属性。对于每个不同的属性,语句都会被执行。
- 2.不建议使用for in 遍历数组,因为输出的顺序是不固定的。
- 3.如果迭代的对象的变量值是null或者undefined, for in不执行循环体,建议在使用for in循环之前,先检查该对象的值是不是null或者undefined
- for of
1.for…of 语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
- 遍历对象
var s = {
a: 1,
b: 2,
c: 3
}
var s1 = Object.create(s);
for (var prop in s1) {
console.log(prop); //a b c
console.log(s1[prop]); //1 2 3
}
for (let prop of s1) {
console.log(prop); //报错如下 Uncaught TypeError: s1 is not iterable
}
for (let prop of Object.keys(s1)) {
console.log(prop); // a b c
console.log(s1[prop]); //1 2 3
}
写一个简单的函数判断类型?
Object.prototype.toString.call()进行判断
3个判断数组的方法,请分别介绍他们之间的区别和优劣
Object.prototype.toString.call() 、 instanceof 以及 Array.isArray()
1. Object.prototype.toString.call() Object.prototype.toString.call() 常用于判断浏览器内置对象。
每一个继承 Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用call或者apply方法来改变toString方法的执行上下文。
const an = ['Hello','An'];
an.toString(); // "Hello,An"
Object.prototype.toString.call(an); // "[object Array]"
这种方法对于所有基本的数据类型都能进行判断,即使是 null 和 undefined
Object.prototype.toString.call('An') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(Symbol(1)) // "[object Symbol]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call({name: 'An'}) // "[object Object]"
2. instanceof
instanceof 的内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
使用 instanceof判断一个对象是否为数组,instanceof 会判断这个对象的原型链上是否会找到对应的 Array 的原型,找到返回 true,否则返回 false。
[] instanceof Array; // true
但 instanceof 只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。
[] instanceof Object; // true
3. Array.isArray()
用来判断对象是否为数组 es6中加⼊了新的判断⽅法
if(Array.isArray(value)){
return true;
}
区别:
- instanceof 与 isArray
当检测Array实例时,Array.isArray 优于 instanceof ,因为 Array.isArray 可以检测出 iframes
- Array.isArray() 与 Object.prototype.toString.call()
Array.isArray()是ES6新增的方法,当不存在 Array.isArray() ,或者
在考虑兼容性的情况下可以⽤toString的⽅法可以用Object.prototype.toString.call()实现。
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
获取首屏时间
1. H5 如果页面首屏有图片
首屏时间 = 首屏图片全部加载完毕的时刻 - performance.timing.navigationStart
2. 如果页面首屏没有图片
首屏时间 = performance.timing.domContentLoadedEventStart - performance.timing.navigationStart
说说你对闭包的理解?
使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。
闭包有三个特性:
- 1.函数嵌套函数
- 2.函数内部可以引用外部的参数和变量
- 3.参数和变量不会被垃圾回收机制回收
用过什么ES6语法
Class- 模块
import和export - 箭头函数
- 函数默认参数
...扩展运输符允许展开数组- 解构
- 字符串模版
Promiselet constProxy、Map、Set
ES6+有什么新特性
- let和const的出现
- let和const区别:const定义一个只读常量,一旦声明变量,就必须立即初始化,不能留到以后赋值且不可改变。
- 不存在变量提升,声明的变量一定要在声明后使用,否则报错
- 不允许重复声明变量,不允许在相同作用域内,重复声明同一个变量
- 块级作用域,ES6之前只有函数作用域与全局作用域,一个大括号即一个块级作用域
- 声明的变量不在属于window
- 解构赋值
数组、对象、字符串等解构赋值的基本用法及默认值的设置
- 字符串的扩展
- 模版字符串
- 方法的增添includes(),startsWith(),endsWith().....
- 正则表达式的具名组匹配
-
数值的扩展
-
函数的扩展
箭头函数
- 数组的扩展
- 扩展运算符:
用途
(1)复制数组
(2)合并数组
(3)函数的rest参数
- 方法的扩展Array.from()、Array.of()实例上的方法fill()、flat()
- 对象的扩展
- 属性以及方法的简洁表示
- 属性名表达式
- 可遍历性for..in、Object.key(obj)
- super关键字的增加
- 新增方法Object.is()、Object.assign()
- Module的导入模块
这一点就很有必要给面试官讲一下ES6模块与CommoonJS模块的差异
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- 其他方面
- promise与async的运用和理解,提及这里他接下来可能会问promise的实现,这也是一道常考题
- Symbol数据类型
- set和map结构
- class
var const let区别
Es6对象扩展了哪些特性?
普通函数function(){} 与 ()=>{}箭头函数的差异
- 更简洁的语法
- 没有this
- 不能使用new 构造函数
- 不绑定arguments,用rest参数...解决
- 使用call()和apply()调用
- 捕获其所在上下文的 this 值,作为自己的 this 值
- 箭头函数没有原型属性
- 不能简单返回对象字面量
- 箭头函数不能当做Generator函数,不能使用yield关键字
- 箭头函数不能换行 总结:
- 箭头函数的 this 永远指向其上下文的 this ,任何方法都改变不了其指向,如 call() , bind() , apply()
- 普通函数的this指向调用它的那个对象
this指向
this的指向不是在编写时确定的,⽽是在执⾏时确定的,同时,this不同的指向在于遵循了⼀定的规则。
- ⾸先,在默认情况下,
this是指向全局对象的,⽐如在浏览器就是指向window。
name = "Bale";
function sayName () {
console.log(this.name);
};
sayName(); //"Bale"
- 其次,如果函数被调⽤的位置存在上下⽂对象时,那么函数是被
隐式绑定的。
function f() {
console.log( this.name );
}
var obj = {
name: "Messi",
f: f
};
obj.f(); //被调⽤的位置恰好被对象obj拥有,因此结果是Messi
- 再次,显示改变this指向,常⻅的⽅法就是
call、apply、bind以bind为例:
function f() {
console.log( this.name );
}
var obj = {
name: "Messi"
};
var obj1 = {
name: "Bale"
};
f.bind(obj)(); //Messi ,由于bind将obj绑定到f函数上后返回⼀个新函数,因此需要再在后⾯加上括号进⾏执⾏,这是bind与apply和call的 区别
- 最后,也是优先级最⾼的绑定
new 绑定。 ⽤ new 调⽤⼀个构造函数,会创建⼀个新对象, 在创造这个新对象的过程中,新对象会⾃动绑定到Person对象的this上, 那么 this ⾃然就指向这个新对象
function Person(name) {
this.name = name;
console.log(name);
}
var person1 = new Person('Messi'); //Messi
绑定优先级: new绑定 > 显式绑定 >隐式绑定 >默认绑定
箭头函数的this指向哪⾥?
箭头函数不同于传统JavaScript中的函数,箭头函数并没有属于⾃⼰的this,它的所谓的this是捕获其所在上下⽂的 this 值,作为⾃⼰的 this 值,并且由于没有属于⾃⼰的this,⽽箭头函数是不会被new调⽤的,这个所谓的this也不会被改变.
我们可以⽤Babel理解⼀下箭头函数:
// ES6 const obj = {
getArrow() {
return () => {
console.log(this === obj);
};
}
}
转化后
// ES5,由 Babel 转译
var obj = {
getArrow: function getArrow() {
var _this = this;
return function () {
console.log(_this === obj);
};
}
};
call/bind/apply区别
相同点
都是改变this指向 第一个参数都是 this 的指向对象。
不同点
call 和 bind 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 。 apply 的所有参数都放在一个数组里面 obj.myFun.apply(db,['成都', ..., 'string' ])。 bind 返回的是一个函数,需要手动调用执行,其他参数和 call 一样。
weak-Set、weak-Map 和 Set、Map 区别?
- Set 对象类似于数组,且成员的值都是唯一的。
- Map 对象是键值对集合,和JSON对象类似,但是key不仅是字符串还可以是对象
async await 和 promise 的关系,及其有缺点?
async await 是 promise 和 generator 函数组合的一个语法糖。async/await是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。毕竟一直then()也很烦。
- async 是异步的意思,而 await 是 async wait的简写,即异步等待。
- 所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。
- 一个函数如果加上 async ,那么该函数就会返回一个 Promise。
sync function test() {
return "1"
}
console.log(test()) // -> Promise {<resolved>: "1"}
可以看到输出的是一个Promise对象。所以,async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve() 封装成Promise对象返回。
相比于 Promise,async/await能更好地处理 then 链
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
现在分别用 Promise 和async/await来实现这三个步骤的处理。
使用Promise
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
使用async/await
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
}
doIt();
await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。
优缺点:
async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
setTimeout、Promise、async/await 三者之间异步解决方案的区别?
手写实现 Promise?
手写 Promise 的 api,以下两个就常常被问到:
- 1. 手写一个 Promise.all
/**
* Promise.all Promise进行并行处理
* 参数: promise对象组成的数组作为参数
* 返回值: 返回一个Promise实例
* 当这个数组里的所有promise对象全部进入FulFilled状态的时候,才会resolve。
*/
Promise.all = function(promises) {
return new Promise((resolve,reject) => {
let values = []
let count = 0
promises.forEach((promise,index) => {
promise.then(value => {
values[index] = value
count ++
if(count === promises.length){
resolve(values)
}
}, reject)
})
})
}
- 2. 手写一个 Promise.race
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
promises.forEach((promise) => {
promise.then(resolve, reject);
})
})
}
Promise经典题目
与js事件循环结合出题,如下,写出执行结果:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {console.log('async2 end')}
async1()
setTimeout(function () {console.log('setTimeout')}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function () {
console.log('promise1')
}).then(function () {
console.log('promise2')
})
console.log('script end')
// 结果如下
// script start
// async2 end
// Promise
// script end
// async1 end
// promise1
// promise2
// setTimeout
实现节流或者防抖代码?
- 防抖函数
function debounce(fn,wait = 50, immediate){
let timer;
return function(){
if(immediate){
fn.apply(this, arguments)
}
if(timer){
clearTimeout(timer)
}
timer = setTimeout(()=>{
fn.apply(this, arguments)
}, wait)
}
}
- 节流函数
function throttle(fn,wait) {
let prev = new Date();
return function() {
const args = arguments;
const now = new Date();
if(now - prev > wait){
fn.apply(this, args);
prev = new Date();
}
}
}
HTML、CSS
rem和em有区别嘛? 1em等于多少像素
rem和em单位一样,都是一个相对单位,不同的是em是相对于元素的父元素的font-size进行计算,rem是相对于 根元素html的font-size进行计算。
移动端适配方案
- rem
- viewport
- vue中使用postcss-pxtorem将px自动转为rem
flex: 0 1 auto 表示什么意思
flex: 0 1 auto 其实就是弹性盒子的默认值,表示 flex-grow, flex-shrink 和 flex-basis 的简写,分别表示放大比例、缩小比例、分配多余空间之前占据的主轴空间。
flex 布局,固定高度,左边定宽,右边自适应
这个比较简单,flex 布局给父元素设置 display:flex,左边的子元素 flex:none,给个 width:100px,右边的子元素 flex:1 就可以了,当然可能有别的方式
怎么让一个div水平垂直居中?最少两种,三种良好,四种优秀
- 绝对定位水平垂直居中
<div style="position: absolute;
width: 500px;
height: 300px;
margin: auto;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: green;">
水平垂直居中
</div>
- 水平垂直居中
<div style="position: relative;
width:400px;
height:200px;
top: 50%;
left: 50%;
margin: -100px 0 0 -200px;
background-color: red;">
水平垂直居中
</div>
- 水平垂直居中
<div style="position: absolute;
width:300px;
height:200px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: blue;">
水平垂直居中
</div>
- flex 布局居中
<div style="display: flex;align-items: center;justify-content: center;">
<div style="width: 100px;height: 100px;background-color: gray;">flex 布局</div>
</div>
display和visibility的区别以及回流和重绘
display:none``会脱离文档流,不占据页面空间;
visibility:hidden,只是隐藏内容,并没有脱离文档流,会占据页面的空间。
讲述回流以及重绘之前需要先了解页面在文档加载完成之后到完全显示中间的过程:
- 根据文档生成DOM树(包括display:none的节点)
- 在DOM树基础上根据节点的几何属性(margin/padding/width/height等)生成render树(不包括display:none、head节点但会包含visibility:hidden节点)
- 在render树基础上进行一步渲染包括color,outline等样式
reflow:当render树中的一部分或者全部因为大小边距等问题发生改变而需要重建的过程叫做回流。
repaint:当元素的一部分属性发生变化,如外观背景色不会引起布局变化而需要重新渲染的过程叫做重绘。
display:none会引起回流和重绘,visibility:hidden会引起重绘
VUE
Vue是如何实现双向绑定的?
利⽤ Object.defineProperty 劫持对象的访问器,在属性值发⽣变化时我们可以获取变化,然后根据变化进⾏后续响应,在 vue3.0中通过Proxy代理对象进⾏类似的操作。
v-bind和v-model的区别?
- v-bind用来绑定数据和属性以及表达式
- v-model使用在表单中,实现双向数据绑定的,在表单元素外不起使用。
v-model的实现原理
从接触Vue我们就知道 v-model是实现数据双向绑定的 那他能实现绑定的原理到底是啥?
v-model原理: 我们在vue项目中主要使用v-model指令在表单 input、textarea、select、等表单元素上创建双向数据绑定, v-model本质上就是vue的语法糖,v-model在内部为不同的输入元素使用不同的属性并抛出不同的事件:
-
text和 textarea元素使用value属性和input事件
-
checkbox和 radio使用checked属性和change事件
-
slect字段将 value作为prop并将change作用事件
<input v-model="something">
本质上相当于这样
<input v-bind:value="something" v-on:input="something = $event.target.value">
// 其实就是通过绑定一个something属性,通过监听input事件,当用户改变输入框数据的时候,
// 通过事件传递过来的事件对象中的target找到事件源,value属性表示事件源的值,从而实现双向数据绑定的效果
v-model 可以看成是 value+input 方法的语法糖(组件)。原生的 v-model ,会根据标签的不同生成不同的事件与属性。解析一个指令来。
自定义:自己写 model 属性,里面放上 prop 和 event
那首先谈谈你对Vue的理解吧?
- 关键点: 渐进式 JavaScript 框架、核心库加插件、动态创建用户界面(异步获取后台数据,数据展示在界面)
- 特点: MVVM 模式;代码简洁体积小,运行效率高,适合移动PC端开发;本身只关注 UI (和 react 相似),可以轻松引入 Vue 插件或其他的第三方库进行开发。
你刚刚说到了MVVM,能详细说说吗?
- MVVM 是 Model-View-ViewModel 的缩写
- Model: 代表数据模型,也可以在Model中定义数据修改和操作的业务逻辑。我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为
- View: 用户操作界面。当ViewModel对Model进行更新的时候,会通过数据绑定更新到View
- ViewModel: 业务逻辑层,View需要什么数据,ViewModel要提供这个数据;View有某些操作,ViewModel就要响应这些操作,所以可以说它是Model for View.
- 总结: MVVM模式简化了界面与业务的依赖,解决了数据频繁更新。MVVM 在使用当中,利用双向绑定技术,使得 Model 变化时,ViewModel 会自动更新,而 ViewModel 变化时,View 也会自动变化。
vue是如何实现响应式数据的呢?(响应式数据原理)
Vue2: Object.defineProperty 重新定义 data 中所有的属性, Object.defineProperty 可以使数据的获取与设置增加一个拦截的功能,拦截属性的获取,进行依赖收集。拦截属性的更新操作,进行通知。 具体的过程:首先Vue使用 initData 初始化用户传入的参数,然后使用 new Observer 对数据进行观测,如果数据是一个对象类型就会调用 this.walk(value) 对对象进行处理,内部使用 defineeReactive 循环对象属性定义响应式变化,核心就是使用 Object.defineProperty 重新定义数据。
vue3.0:
vue中是如何检测数组变化的
数组就是使用 object.defineProperty 重新定义数组的每一项,那能引起数组变化的方法我们都是知道的, pop 、 push 、 shift 、 unshift 、 splice 、 sort 、 reverse 这七种,只要这些方法执行改了数组内容,我就更新内容就好了,是不是很好理解。
是用来函数劫持的方式,重写了数组方法,具体呢就是更改了数组的原型,更改成自己的,用户调数组的一些方法的时候,走的就是自己的方法,然后通知视图去更新。
数组里每一项可能是对象,那么我就是会对数组的每一项进行观测,(且只有数组里的对象才能进行观测,观测过的也不会进行观测)
vue3:改用 proxy ,可直接监听对象数组的变化。
了解nextTick吗
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout、promise那些),定义了一个异步方法,多次调用nextTick会将方法存入队列,通过异步方法清空当前队列。
nextTick是Vue提供的一个全局API。
由于vue的异步更新策略导致我们对数据的修改不会立刻体现在dom变化上。
此时如果想要立即获取更新后的dom状态,就需要使用这个方法。
Vue 在更新 DOM 时是异步执行的。
只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。
这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
nextTick方法会在队列中加入一个回调函数,确保该函数在前面的dom操作完成后才调用。
所以当我们想在修改数据后立即看到dom执行结果就需要用到nextTick方法。
nextTick的内部实现原理
vue中$nexTick有什么用?
在Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中。原因是什么呢,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中
使用场景: 一个子组件通过v-if控制隐藏显示,当修改完显示状态后,立马通过ref去操作子组件的方法,这个时候会报错,原因在于子组件此时可能还未渲染完成,这个时候使用nextTick可以解决,他会在dom更新完成之后再去调用。
作用: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
vue中$set有什么用?
在我们使用vue进行开发的过程中,可能会遇到一种情况:当生成vue实例后,当再次给数据赋值时,有时候并不会自动更新到视图上去,因此可以使用$set。
initTableData() {
this.tableData.forEach(element => {
this.$set(element, 'edit', false)
})
}
事件循环机制
执行顺序:整个脚本-->异步任务
异步任务: 分为宏任务和微任务,先执行微任务再执行宏任务
宏任务:script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering
微任务:MutationObserver、Promise.then()或reject()
注意:遇到async函数的时候,await之后的代码属于微任务,相当于promise.then
其中,new Promise为立即执行函数。
接下来看几个例子
async function a() {
console.log('a')
await b()
console.log('a end')
}
async function b() {
console.log('b')
}
a()
setTimeout(function () {
console.log('setTimeout')
}, 0)
new Promise(function (resolve, reject) {
console.log('promise')
resolve()
}).then(function () {
console.log('then')
})
console.log('main end')
// a
// b
// promise
// main end
// a end
// then
// setTimeout
setTimeout(function () {
console.log('8')
}, 0)
async function async1() {
console.log('1')
const data = await async2()
console.log('6')
return data
}
async function async2() {
return new Promise(resolve => {
console.log('2')
resolve('async2的结果')
}).then(data => {
console.log('4')
return data
})
}
async1().then(data => {
console.log('7')
console.log(data)
})
new Promise(function (resolve) {
console.log('3')
resolve()
}).then(function () {
console.log('5')
})
// 1 2 3 4 5 6 7 async的结果 8
说说vue的生命周期
Vue实例的生命周期讲一下, mounted阶段真实DOM存在了嘛?
Vue实例从创建到销毁的过程,就是生命周期。 也就是:开始创建->初始化数据->编译模板->挂载dom->数据更新重新渲染虚拟 dom->最后销毁。这一系列的过程就是vue的生命周期。所以在mounted阶段真实的DOM就已经存在了。
1. 什么时候被调用?
- beforeCreate :实例初始化之后,数据观测之前调用。(vue实例的挂载元素el和数据对象data都还没有进行初始化,还是一个 undefined状态)
- created:实例创建万之后调用。实例完成:数据观测、属性和方法的运算、 watch/event 事件回调。无 $el . (此时vue实例的数据对象data已经有了,可以访问里面的数据和方法, el还没有,也没有挂载dom)
- beforeMount:在挂载之前调用,相关 render 函数首次被调用。(在这里vue实例的元素el和数据对象都有了,只不过在挂载之前还是虚拟的 dom节点)
- mounted:了被新创建的vm.$el替换,并挂载到实例上去之后调用改钩子。(vue实例已经挂在到真实的dom上,可以通过对 dom操作来获取dom节点)
- beforeUpdate:数据更新前调用,发生在虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
- updated:由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用改钩子。
- beforeDestroy:实例销毁前调用,实例仍然可用。(vue实例在销毁前调用,在这里还可以使用,通过this也能访问到实例,可以在这里对一些不用的定时器进行清除,解绑事件。)
- destroyed:实例销毁之后调用,调用后,Vue实例指示的所有东西都会解绑,所有事件监听器和所有子实例都会被移除
2. 每个生命周期内部可以做什么?
-
created:实例已经创建完成,因为他是最早触发的,所以可以进行一些数据、资源的请求。
-
mounted:实例已经挂载完成,可以进行一些DOM操作。
-
beforeUpdate:可以在这个钩子中进一步的更改状态,不会触发重渲染。
-
updated:可以执行依赖于DOM的操作,但是要避免更改状态,可能会导致更新无线循环。
-
destroyed:可以执行一些优化操作,清空计时器,解除绑定事件。 3. ajax放在哪个生命周期?
-
一般放在
mounted中,保证逻辑统一性,因为生命周期是同步执行的, ajax 是异步执行的。单数服务端渲染 ssr 同一放在 created 中,因为服务端渲染不支持 mounted 方法。 4. 什么时候使用beforeDestroy?
当前页面使用 $on ,需要解绑事件。清楚定时器。解除事件绑定, scroll mousemove 。
Vuex 工作原理
vuex是一个专门为vue.js开发的状态管理模式,每一个vuex应用核心就是store(仓库)。store基本上就是一个容器,它包含着你的应用中大部分的state(状态)。
- vuex的状态存储是响应式的,当 vue组件中store中读取状态时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 改变store中的状态的唯一途径就是显示 commit(提交)mutation,这样使得我们可以方便地跟踪每一个状态的变化。
状态自管理应用包含以下几个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。下图单向数据流示意图:
主要有以下几个模块:
- State: 定义了应用状态的数据结构,可以在这里设置默认的初始状态
- Getter: 允许组件从Stroe中获取数据, mapGetters辅助函数仅仅是将store中的 getter映射到计算属性。
- Mutation: 唯一更改store中状态的方法,且必须是同步函数。
- Action: 用于提交muatation, 而不是直接变更状态,可以包含任意异步操作。
- Module: 允许将单一的store拆分为多个 sotre且同时保存在单一的状态树中
简单来说: 是一个能方便vue实例及其组件传输数据的插件 方便传输数据,作为公共存储数据的一个库
- state: 状态中心
- mutations: 更改状态,同步的
- actions: 异步更改状态
- getters: 获取状态
- modules: 将state分成多个modules,便于管理
应用场景:单页应用中,组件之间的状态。音乐播放、登录状态、加入购物车。
网上找的一个通俗易懂的了解vuex的例子 公司有个仓库
- 1.State(公司的仓库)
- 2.Getter(只能取出物品,包装一下,不能改变物品任何属性)
- 3.Muitation(仓库管理员,只有他可以直接存储到仓库)
- 4.Action(公司的物料采购员,负责从外面买东西和接货, 要往仓库存东西,告诉仓库管理员要存什么) 非常要注意的地方:只要刷新或者退出浏览器,仓库清空。
Vue组件如何通信
常见使用场景可以分为三类:
- 父子组件通信:
props; $parent / $children; provide / inject ; ref ; $attrs / $listeners - 兄弟组件通信:
eventBus ; vuex - 跨级通信:
eventBus;Vuex;provide / inject 、$attrs / $listenersvue中8种组件通信方式
computed和watch有什么区别?
computed:
computed是计算属性,也就是计算值,它更多⽤于计算值的场景computed具有缓存性,computed的值在getter执⾏后是会缓存的,只有在它依赖的属性值改变之后,下⼀次获取computed的值时才会重新调⽤对应的getter来计算computed适⽤于计算⽐较消耗性能的计算场景 watch:- 更多的是
「观察」的作⽤,类似于某些数据的监听回调,⽤于观察props $emit或者本组件的值,当数据变化时来执 ⾏回调进⾏后续操作 ⽆缓存性,⻚⾯重新渲染时值不变化也会执⾏ ⼩结:- 当我们要进⾏数值计算,⽽且依赖于其他数据,那么把这个数据设计为computed
- 如果你需要在某个数据变化时做⼀些事情,使⽤watch来观察这个数据变化
v-if 和 v-show 区别
- v-if 如果条件不成立不会渲染当前指令所在节点的DOM元素
- v-show 只是切换当前DOM的显示与隐藏
v-for和v-if为什么不能连用
v-for 会比 v-if 的优先级更高,连用的话会把 v-if 的每个元素都添加一下,造成性能问题。
组件中的data为什么是函数
因为组件是可以复用的,js里对象是引用关系,如果组件data是一个对象,那么子组件中的data属性值会互相污染,产生不必要的麻烦。所以一个组件中的data必须是一个函数,因此每个实例可以维护一份被返回对象独立的拷贝。也因为new Vue的实例是不会被复用,所以不存在以上问题。
介绍一下Vue中的Diff算法?
在新老虚拟DOM对比时
- 首先,对比节点本身,判断是否为同一节点,如果不为相同节点,则删除该节点重新创建节点进行替换
- 如果为相同节点,进行patchVnode,判断如何对该节点的子节点进行处理,先判断一方有子节点一方没有子节点的情况(如果新的children没有子节点,将旧的子节点移除)
- 比较如果都有子节点,则进行updateChildren,判断如何对这些新老节点的子节点进行操作(diff核心)。
- 匹配时,找到相同的子节点,递归比较子节点 在diff中,只对同层的子节点进行比较,放弃跨级的节点比较,使得时间复杂从O(n^3)降低值O(n),也就是说,只有当新旧children都为多个子节点时才需要用核心的Diff算法进行同层级比较。
key属性的作用是什么
key 是为Vue中的vnode标记的唯⼀id,通过这个key,我们的diff操作可以更准确、更快速 diff算法的过程中,先会进⾏新旧节点的⾸尾交叉对⽐,当⽆法匹配的时候会⽤新节点的 key 与旧节点进⾏⽐对,然后查出差异
- 准确: 如果不加 key ,那么vue会选择复⽤节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产⽣⼀系列 的bug.
- 快速: key的唯⼀性可以被Map数据结构充分利⽤,相⽐于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1).
slot是什么?有什么作用?原理是什么?
slot又名插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot又分三类,默认插槽,具名插槽和作用域插槽。
- 默认插槽:又名匿名查抄,当slot没有指定name属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
- 具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.slot.default,具名插槽为vm.slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
action 与 mutation 的区别
- mutation 是同步更新, $watch 严格模式下会报错
- action 是异步操作,可以获取数据后调用 mutation 提交最终数据
谈谈对keep-alive的了解
-
生命周期: 当引入
keep-alive的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。 -
基本用法:可以将 是否包裹
keep-alive通过参数配置。
//不需要刷新的路由配置里面配置
meta: {keepAlive: true}, 这个路由则显示在上面标签;
//需要刷新的路由配置里面配置
meta: {keepAlive: false}, 这个路由则显示在下面标签;
- 作用:
返回
dom不让其重新刷新,在vue-view外面包一层, 当引入keep-alive的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。
事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中;
- 何时刷新
在
keep-alive中直接添加include,cachedViews(Array类型:包含vue文件的组件name都将被缓存起来);反之exclude则是不包含;
说说Vue2.0和Vue3.0有什么区别
1. 重构响应式系统,使用Proxy替换Object.defineProperty,使用Proxy优势:
- 可直接监听数组类型的数据变化
- 监听的目标为对象本身,不需要像Object.defineProperty一样遍历每个属性,有一定的性能提升
- 可拦截apply、ownKeys、has等13种方法,而Object.defineProperty不行
- 直接实现对象属性的新增/删除 2. 新增Composition API,更好的逻辑复用和代码组织
3. 重构 Virtual DOM
- 模板编译时的优化,将一些静态节点编译成常量
- slot优化,将slot编译为lazy函数,将slot的渲染的决定权交给子组件
- 模板中内联事件的提取并重用(原本每次渲染都重新生成内联函数) 4. 代码结构调整,更便于Tree shaking,使得体积更小
5. 使用Typescript替换Flow
Proxy与Object.defineProperty的区别
Proxy的优势如下:
- Proxy可以直接监听对象⽽⾮属性
- Proxy可以直接监听数组的变化
- Proxy有多达13种拦截⽅法,不限于
apply、ownKeys、deleteProperty、has等等是 Object.defineProperty 不具备的 - Proxy返回的是⼀个新对象,我们可以只操作新的对象达到⽬的,⽽ Object.defineProperty 只能遍历对象属性直接修改
- Proxy作为新标准将受到浏览器⼚商重点持续的性能优化,也就是传说中的新标准的性能红利 Object.defineProperty的优势如下:
- 兼容性好,⽀持IE9
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
Object.defineProperty本身有一定的监控到数组下标变化的能力:
Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。
直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
由于只针对了以上几种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
而要取代它的Proxy有以下两个优点:
可以劫持整个对象,并返回一个新对象
有13种劫持操作
如何扩展某个Vue组件?
常用的组件扩展的方法有:mixins,solts,extends 混入mixins是分发Vue组件中可符用功能的非常灵活的方式。混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被混入该组件本身的选项。
// 复用代码:它是一个配置对象,选项和组件里面一样
const myminxin = {
methods: {
dosomething(){}
}
}
// 全局混入:将混入对象传入
Vue.mixin(mymixin)
//局部混入:做到数组项设置到minxins选项,仅作用于当前组件
const Comp = {
mixins: [mymixin]
}
插槽主要用于vue组件中的内容分发,也可以用于组件扩展。 子组件Child
<div>
<slot>这个内容会被父组件传递的内容替换。</slot>
</div>
父组件Parent
<div>
<Child>来自老爹的内容</Child>
</div>
如果要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽。
vue3.0有什么新特性
这边我就不总结了,直接引用大佬总结的
Vue性能优化
1. 编码优化:
- keep-alive
- 拆分组件
- key 保证唯一性
- 路由懒加载、异步组件
- 防抖节流 2. Vue加载性能优化
- 第三方模块按需导入( babel-plugin-component )
- 图片懒加载
Vue Router
vue-route是怎么实现的?
- vue-router通过hash与History interface两种方式实现前端路由,更新视图但不重新请求页面”是前端路由原理的核心之一
- hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。
- history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。
router 和 route 分别是什么
router 为 VueRouter 的实例,是一个全局路由对象,包含了路由跳转的方法、钩子函数等。
route 是路由信息对象||跳转的路由对象,每一个路由都会有一个route对象, 是一个局部对象,包含path,params,hash,query,fullPath,matched,name等路由信息参数
如何传参?
- Params
只能使用
name,不能使用path,参数不会显示在路径上,浏览器强制刷新参数会被清空。 - Query: 参数会显示在路径上,刷新不会被清空,name 可以使用path路径。
动态路由定义和获取
在router目录下的index.js文件中,对path属性加上/:id。 使用router对象的params.id获取
vue-router有哪几种导航钩子?
参数:有to(前往的路由)、from(离开的路由)、next(可以在beforeEach改变或中断导航)
- 全局导航钩子(跳转前进行判断拦截)
next必须被调用, 作用:跳转前进行拦截判断
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
- 路由独享守卫
你可以在路由配置上直接定义
beforeEnter守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
// ...
}
}
]
})
- 组件内守卫
beforeRouteEnterbeforeRouteUpdate(2.2 新增)beforeRouteLeave
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}
不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
框架
mvvm和mvc模型区别
MVC:MVC模式可以这样理解,将html看成view;js看成controller,处理用户与应用的交互,响应对view的操作(对事件的监听),调用Model对数据进行操作,完成model与view的同步(根据model的改变,通过选择器对view进行操作);将js的ajax当做Model,从服务器获取数据,MVC是单向的。 MVVM:它实现了View和Model的自动同步,也就是当Model的属性改变时,我们不用再自己手动操作Dom元素,来改变View的显示,而是改变属性后该属性对应View层显示会自动改变,MVVM是双向的。
mvvm 的实现
Vue与Angular以及React的区别?
1. Vue与AngularJS的区别
- Angular采用TypeScript开发, 而Vue可以使用javascript也可以使用TypeScript
- AngularJS依赖对数据做脏检查,所以Watcher越多越慢;Vue.js使用基于依赖追踪的观察并且使用异步队列更新,所有的数据都是独立触发的。
- AngularJS社区完善, Vue的学习成本较小
2. Vue与React的区别
- vue组件分为全局注册和局部注册,在react中都是通过import相应组件,然后模版中引用;
- props是可以动态变化的,子组件也实时更新,在react中官方建议props要像纯函数那样,输入输出一致对应,而且不太建议通过props来更改视图;
- 子组件一般要显示地调用props选项来声明它期待获得的数据。而在react中不必需,另两者都有props校验机制;
- 每个Vue实例都实现了事件接口,方便父子组件通信,小型项目中不需要引入状态管理机制,而react必需自己实现;
- 使用插槽分发内容,使得可以混合父组件的内容与子组件自己的模板;
- 多了指令系统,让模版可以实现更丰富的功能,而React只能使用JSX语法;
- Vue增加的语法糖computed和watch,而在React中需要自己写一套逻辑来实现;
- react的思路是all in js,通过js来生成html,所以设计了jsx,还有通过js来操作css,社区的styled-component、jss等;而 vue是把html,css,js组合到一起,用各自的处理方式,vue有单文件组件,可以把html、css、js写到一个文件中,html提供了模板引擎来处理。
- react做的事情很少,很多都交给社区去做,vue很多东西都是内置的,写起来确实方便一些, 比如 redux的combineReducer就对应vuex的modules, 比如reselect就对应vuex的getter和vue组件的computed, vuex的mutation是直接改变的原始数据,而redux的reducer是返回一个全新的state,所以redux结合immutable来优化性能,vue不需要。
- react是整体的思路的就是函数式,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以做到,比如结合redux-form,组件的横向拆分一般是通过高阶组件。而vue是数据可变的,双向绑定,声明式的写法,vue组件的横向拆分很多情况下用mixin。
计算机网络
介绍下如何实现token加密的?
cookie 和 token 都存放在 header 中,为什么不会劫持 token?
1、首先token不是防止XSS的,而是为了防止CSRF的; 2、CSRF攻击的原因是浏览器会自动带上cookie,而浏览器不会自动带上token
从 url 到页面显示
- 浏览器的地址栏输入URL并按下回车。
- 浏览器查找当前URL是否存在缓存,并比较缓存是否过期。
- DNS解析URL对应的IP。
- 根据IP建立TCP连接(三次握手)。
- HTTP发起请求。
- 服务器处理请求,浏览器接收HTTP响应。
- 渲染页面,构建DOM树。
- 关闭TCP连接(四次挥手)。
为什么有了HTTP为什么还要HTTPS?
https是安全版的http,因为http协议的数据都是明⽂进⾏传输的,所以对于⼀些敏感信息的传输就很不安全,HTTPS就是为了解决HTTP的不安全⽽⽣的。
http和https的区别
- Http是超文本传输协议,数据明文传输。
- Https则是具有安全性的SSL加密传输协议,传输是加密了的。
- Http和Https使用的端口不一样,前者是80,后者是443。
常见的HTTP状态码都有那些?
HTTP网络状态码(STATUS) 根据状态码能够清楚的反映出当前交互的结果及原因
- 1xx:指示信息–表示请求已接收,继续处理。
- 2xx:成功–表示请求已被成功接收、理解、接受。
- 3xx:重定向–要完成请求必须进行更进一步的操作。
- 4xx:客户端错误–请求有语法错误或请求无法实现。
- 5xx:服务器端错误–服务器未能实现合法的请求。 平常遇见的状态码:
- 200 OK 成功(最理想的状态)
- 301 Moved Permanently 永久转移(永久重定向),资源已经被分配了新的url
- 302 Move temporarily 临时转移(临时重定向), 资源临时被分配了新的url
- 303 see other,表示资源存在着另⼀个 URL,应使⽤ GET ⽅法重新获取资源
- 304 Not Modified 表示服务器允许访问资源,但因发⽣请求未满⾜条件的情况
- 400 Bad Request 请求参数错误
- 401 Unauthorized 无权限访问
- 403 forbidden,表示对请求资源的访问被服务器拒绝 ✨
- 404 Not Found 找不到资源(最不理想的状态)
- 405 Method Not Allowed 请求行中指定的请求方法不能被用于请求相应的资源,但是该响应必须返回一个Allow头信息来表示出当前资源能够接受请求方法的列表。
- 408 Request timeout, 客户端请求超时
- 409 Confict, 请求的资源可能引起冲突
- 500 Internal Server Error 未知的服务器错误
- 503 Service Unavailable 服务器超负荷
2xx状态码一般是前端人员的锅,5xx一般是后台人员的锅,学会看问题出在哪里很重要,对以后工作中的甩锅有很大帮助。
浏览器缓存知道吗
浏览器缓存也就是HTTP缓存机制,其机制是根据HTTP报文的缓存标识进行的,缓存分为两种:
- 强制缓存
当浏览器向服务器发送请求的时候,服务器会将缓存规则放入HTTP响应的报文的HTTP头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是Expires和Cache-Control,其中Cache-Conctrol的优先级比Expires高。
Expires是一个绝对时间,即服务器时间。浏览器检查当前时间,如果还没到失效时间就直接使用缓存文件。但是该方法存在一个问题:服务器时间与客户端时间可能不一致。因此该字段已经很少使用。
cache-control中的max-age保存一个相对时间。例如Cache-Control: max-age = 484200,表示浏览器收到文件后,缓存在484200s内均有效。如果同时存在cache-control和Expires,浏览器总是优先使用cache-control。
- 协商缓存 (对比缓存)
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程
对比缓存通过HTTP的 last-modified,Etag 字段进行判断。
强制缓存优先于协商缓存进行,若强制缓存生效则直接使用缓存,若不生效则进行协商缓存,协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回304,继续使用缓存
什么文件用强缓存,什么文件用协商缓存?
TCP是如何发起连接和关闭连接的?
三次握手,四次挥手
解释 TCP/IP 的三次握手和四次挥手
三次握手:
为了准确无误地把数据送达目标处,TCP协议采用了三次握手策略。用TCP协议把数据包送出去后,它一定会向对方确认是否成功送达。握手过程中使用了TCP的标志:SYN和ACK。 发送端首先发送一个带SYN标志的数据包给对方。接收端收到后,回传一个带有SYN和ACK标志的数据包以示传达确认信息。最后,发送端再回传一个带ACK标志的数据包,代表“握手”结束 若在握手过程中某个阶段莫名中断,TCP协议会再次以相同的顺序发送相同的数据包。
GET 和 POST 区别?
- 数据传输⽅式不同:
GET请求通过URL传输数据,⽽POST的数据通过请求体传输。 - 安全性不同:
POST的数据因为在请求主体内,所以有⼀定的安全性保证,⽽GET的数据在URL中,通过历史记录,缓存很容易查到数据信息。 - 数据类型不同:
GET只允许ASCII 字符,⽽POST⽆限制。 - GET⽆害: 刷新、后退等浏览器操作GET请求是⽆害的,
POST可能重复提交表单 - 特性不同:GET是安全(这⾥的安全是指只读特性,就是使⽤这个⽅法不会引起服务器状态变化)且幂等(幂等的 概念是指同⼀个请求⽅法执⾏多次和仅执⾏⼀次的效果完全相同),⽽POST是⾮安全⾮幂等
后端能删除前端设置的cookie吗
可以,用cookie,就让cookie过期好了,设置会话过期,
解释跨域问题以及前端常用的解决方案
1. json: 不仅仅是script标签不受同源策略影响,实际上jsonp是一种前后端约定的解决方案。
不过现在基本已经很少用到了。因为现在已经有了更流行的CORS方案,相对来说也会更安全,不过jsonp还是有其自身的优势的。 很多人都知道浏览器的同源策略,就是发送请求的页面地址和被请求的接口地址的域名,协议,端口三者必须一致,否则浏览器就会拦截这种请求。浏览器拦截的意思不是说请求发布出去,请求还是可以正常触达服务器的,如果服务器正常返回了浏览器也会接收的到,只是不会交给我们所在的页面。这一点查看network是可以看到的。 jsonp一般是利用script标签的src属性,对于服务器来说只有请求和响应两种操作,请求来了就会响应,无论响应的是什么。请求的类型实在太多了。 浏览器输入一个url是一个请求,ajax调用一个接口也是一个请求,img和script的src也是请求。这些地址都会触达服务器。那为什么jsonp一般会选用script标签呢,首先大家都知道script加载的js是没有跨域限制的,因为加载的是一个脚本,不是一个ajax请求。你可以理解为浏览器限制的是XMLHttpRequest这个对象,而script是不使用这个对象的。 仅仅没有限制还不够,还有一个更重要的点因为script是执行js脚本的标签,他所请求到的内容会直接当做js来执行。 这也可以看出,jsonp和ajax对返回参数的要求是不同的,jsonp需要服务返回一段js脚本,ajax需要返回的是数据。 因此这就要求服务器单独来处理jsonp这中请求,一般服务器接口会把响应的数据通过函数调用的方式返回
2. CORS: 浏览器通过同源策略来限制前后端的跨域问题,但同时也给了相应的解决方案。服务器在返回相应的时候可以通过设置响应头来允许哪些网址跨域请求,这样前端就可以成功拿到响应的结果了。所以这也证实了,前端拿不到结果不是服务器不返回,而是浏览器没有给到前端。
Access-Control-Allow-Origin: www.xxxx.com
3. webpack的proxy:前面说了,跨域是因为浏览器的同源策略限制,问题发生在浏览器身上,那我们是不是可以避过浏览器呢。
我们在使用webpack开发项目的时候,webpack的dev-server模块会启动一个服务器,这个服务器不止帮我们做了自动更新,同时也可以做到反向代理。就是我们把请求发送给webpack-dev-server, 然后webpack-dev-server再去请求后端服务器,服务之间的请求是没有跨域问题的,只要后端返回了webpack-dev-server就能拿到,然后再返回给前端。
CORS 的细节,哪些是简单请求?哪些是非简单请求
localstorage、sessionStorage 和 cookie 的区别
- 数据存储方面:cookie在同源的HTTP请求里,在服务器和客户端来回传递。storage是本地保存。
- 存储数据大小:cookie限制4kb,storage约5MB。
- 数据有效期:cookie的有效期与过期时间设置有关(默认是会话),sessionStorage 当前标签页有效, localStorage始终有效。
- 作用域:cookie、localStorage同源窗口,sessionStorage当前标签页
- 操作:cookie只作为document的一个属性可获取,没有其他操作方法。storage有getItem、setItem、removeItem、clear等方法
cookie 跨域时候要如何处理
介绍下webScoket?
主要讲了他两三个特点,以及工作中哪里用到了它。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。 其他特点包括:
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
- 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
CDN
- cnd是什么: cdn是内容分发网络,它利用了http的缓存机制,代理源站相应客户端给的请求。
- cdn原理:
dns 、负载均衡 、缓存
- 首先经过本地的
dns解析,请求cname指向的那台cdn专用的dns服务器。- dns服务器返回全局负载均衡的服务器ip给用户
- 用户请求全局负载均衡服务器,服务器根据ip返回所在区域的负载均衡服务器ip给用户
- 用户请求区域负载均衡服务器,负载均衡服务器根据用户ip选择距离近的,并且存在用户所需内容的,负载比较合适的一台缓存服务器ip给用户。当没有对应内容的时候,会去上一级缓存服务器去找,直到找到资源所在的源站服务器,并且缓存在缓存服务器中。用户下一次在请求该资源,就可以就近拿缓存了。
请求优化
讲请求优化的之前先来总js, css文件顺序优化,为了让渲染更快,我们需要把js放到尾部,css放到头部,然后还要注意在书写js的时候尽量减少重排,重绘。书写html,css的时候尽量简洁,不要冗余,目的是为了更快的构建DOM树和CSSOM树。
说说请求优化,请求优化可以从请求数量和请求时间两方面入手
1. 减少请求数量
- 将小图片打包成base64
- 利用雪碧图融合多个小图片
- 利用缓存上面已经说到过 2. 减少请求时间
- 将js,css,html等文件能压缩的尽量压缩,减少文件大小,加快下载速度
- 利用webpack打包根据路由进行懒加载,不要初始就加载全部,那样文件会很大
- 能升级到高版本的http就升级到高版本
- 建立内部CDN能更快速的获取文件
前端安全
有哪些可能引起前端安全的问题
- 跨站脚本 (Cross-Site Scripting, XSS): ⼀种代码注⼊⽅式,常⻅于⽹络论坛, 起因是⽹站没有对⽤户的输⼊进⾏严格的限制, 使得攻击者可以将脚本上传到帖⼦让其他⼈浏览到有恶意脚本的⻚⾯, 其注⼊⽅式很简单包括但
不限于 JavaScript / VBScript / CSS / Flash等 - iframe的滥⽤: iframe中的内容是由
第三⽅来提供的,默认情况下他们不受我们的控制,他们可以在iframe中运⾏JavaScirpt脚本、Flash插件、弹出对话框等等,这可能会破坏前端⽤户体验 - 跨站点请求伪造(Cross-Site Request Forgeries,CSRF): 指攻击者通过设置好的陷阱,强制对已完成认证的⽤户进⾏⾮预期的个⼈信息或设定信息等某些状态更新,属于被动攻击
- 恶意第三⽅库: ⽆论是后端服务器应⽤还是前端应⽤开发,绝⼤多数时候我们都是在借助开发框架和各种类库进⾏快速开发,⼀旦第三⽅库被植⼊恶意代码很容易引起安全问题
CSRF是什么?
node 和后端知识
什么是 yarn 和 npm?为什么要用 yarn 代替 npm 呢?
- npm 是与 Node.js 自带的默认包管理器,它有一个大型的公共库和私有库,存储在 npm registry 的数据库中(译者注,官方默认中心库 registry.npmjs.org/,国内淘宝镜像 registry.npm.taobao.org/),用户可以通过 npm 命令行访问该数据库。在 npm 的帮助下,用户可以轻松管理项目中的依赖项。
- yarn 也是一个包管理器,为了解决 npm 的一些缺点。yarn 依赖 npm 注册中心为用户提供对包访问。yarn 底层结构基于 npm,如果从 npm 迁移到 yarn,项目结构和工作流不需要大改。
- 就像之前提到的,在某些情况下,yarn 提供了比 npm 更好的功能。与 npm 不同的是,它会缓存下载的每个包,不必重新下载。
- 通过校验和验证包的完整性来提供更好的安全性,保证在某个系统上运行的包在任何其他系统中的工作方式完全相同,这就是为什么选择 yarn 而不是 npm 来进行包管理。
前端工程化
Common.js 和 es6 module 区别
- commonJs 是被加载的时候运行,esModule 是编译的时候运行
- commonJs 输出的是值的浅拷贝,esModule 输出值的引用
- webpack 中的 webpack_require 对他们处理方式不同
- webpack 的按需加载实现
介绍模块化发展历程
可从AMD、CMD、CommonJS、webpack(require.ensure)、ES Module、
解答:
- AMD: 使用requireJS 来编写模块化,特点:依赖必须提前声明好
define('./index.js',function(code){
// code 就是index.js 返回的内容
})
- CMD: 使用seaJS 来编写模块化,特点:支持动态引入依赖文件。
define(function(require, exports, module) {
var indexCode = require('./index.js');
});
- CommonJS:服务端规范 , nodejs 中自带的模块化。
var fs = require('fs');
- 一个文件就是一个模块
- 每个模块都有单独的作用域
- 通过
module.exports导出成员- 通过
require函数载入模块- commonjs是以同步的方式加载模块 node的执行机制是在启动时去加载模块 在执行阶段不需要加载模块
- CommonJS 模块输出的是一个值的拷贝,一旦输出一个值,模块内部的变化就影响不到这个值
- CommonJS 模块加载的顺序,按照其在代码中出现的顺序
- 由于 CommonJS 是同步加载模块的,在服务器端,文件都是保存在硬盘上,所以同步加载没有问题,但是对于浏览器端,需要将文件从服务器端请求过来,那么同步加载就不适用了,所以,CommonJS 是不适用于浏览器端的。
- CommonJS 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
-
webpack(require.ensure):webpack 2.x 版本中的代码分割。
-
ES Modules: ES6 引入的模块化,支持import 来引入另一个 js 。
import a from 'a';
webpack是什么?
webpack是个静态模块打包工具。 在webpack看来,项目里所有资源皆模块,利用资源依赖关系,把各模块之间关联起来。 简单讲就:webpack 对有依赖关系的多个模块文件进行打包处理后,生成浏览器可以直接 高效运行的资源。通过 入口文件 开始,利用 递归 找到直接依赖或间接依赖的所有模块,并在内部构建一个能映射出项目所需的所有模块的 依赖图,并进行webpack打包生成一个或多个bundle文件。
bundle,chunk,module 是什么?
bundle:是由webpack打包出来的文件,chunk:代码块,一个 chunk 由多个模块组合而成,用于代码的合并和分割。module:是开发中的单个模块,在 webpack 的世界,一切皆模块,一个模块对应一个文件,webpack会从配置的entry中递归开始找出所有依赖的模块。
webpack热更新原理
热更新又称热替换(Hot Module Replacement),缩写为
HMR,基于webpack-dev-server。 浏览器的网页通过websocket协议与服务器建立起一个长连接,并监听本地文件的改动; 当服务器的css/js/html进行了修改的时候,服务器会向前端发送一个更新的消息,如果是css或者html发生了改变,网页执行js直接操作dom,局部刷新,如果是js发生了改变,只能刷新整个页面。
讲讲 webpack 的性能优化
⽤webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运⾏快速⾼效。
- 压缩代码: 删除多余的代码、注释、简化代码的写法等等⽅式。可以利⽤
webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS⽂件, 利⽤ cssnano (css-loader?minimize)来压缩css - 利⽤CDN加速: 在构建过程中,将引⽤的静态资源路径修改为CDN上对应的路径。可以利⽤webpack对 于 output 参数和各loader的 publicPath 参数来修改资源路径
- Tree Shaking: 将代码中永远不会⾛到的⽚段删除掉。可以通过在启动webpack时追加参数 --optimize-minimize 来 实现
- Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利⽤浏览器缓存 提取公共第三⽅库: SplitChunksPlugin插件来进⾏公共模块抽取,利⽤浏览器缓存可以⻓期缓存这些⽆需频繁变动的 公共代码
1. 基础配置优化
- extensions 这个配置是属于resolve里面的,经常用来对文件后缀进行扩展,写法如下
resolve: {
extensions: ['.ts', '.tsx', '.js']
}
这个配置表示webpack会根据extensions去寻找文件后缀名,所以如果我们的项目主要用ts写的话,那我们就可以.tsx和.ts写前面,目的是为了让webpack能够快速解析
- alias 这个配置也是属于resolve里面的,是用来映射路劲,能减少打包时间的主要原因是能够让webpack快速的解析文件路径,找到对应的文件,配置如下:
resolve: {
alias: {
Components: path.resolve(__dirname, './src/components')
}
}
- noParse表示不需要解析的文件,有的文件可能是来自第三方的文件,被 providePlugin引入作为windows上的变量来使用,这样的文件相对比较大,并且已经是被打包过的,所以把这种文件排除在外是很有必要的,配置如下:
module: {
noParse: [/proj4\.js/]
}
- exclude 某些loader会有这样一个属性,目的是指定loader作用的范围,exclude表示排除某些文件不需要babel-loader处理,loader的作用范围小了,打包速度自然就快了,用babel-loader举一个简单例子:
{
test: /\.js$/,
loader: "babel-loader",
exclude: path.resolve(__dirname, 'node_modules')
}
- devtool 这个配置是一个调试项,不同的配置展示效果不一样,打包大小和打包速度也不一样,比如开发环境下cheap-source-map肯定比source-map快,
{
devtool: 'cheap-source-map'
}
- .eslintignore 这个虽不是webpack配置但是对打包速度优化还是很有用的,在实践中eslint检查对打包的速度影响很大,但是很多情况我们不能没有这个eslint检查,eslint检查如果仅仅在vs里面开启的话,可能不怎么保险。 因为有可能你vs中的eslint插件突然关闭了或者某些原因vs不能检查了,只能靠webpack构建去帮你拦住错误代码的提交,即使这样还不能确保万无一失,因为你可能某一次提交代码很急没有启动服务,直接盲改提交上去了。这个时候只能通过最后一道屏障给你保护,比如在jenkins构建的时候帮你进行eslint检查,三道屏障确保了我们最终出的镜像是不会有问题的。 所以eslint是很重要的,不能删掉,在不能删掉的情况下怎么让检查的时间更少了,我们就可以通过忽略文件,让不必要的文件禁止eslint,只对需要的文件eslint可以很大程度提高打包速度。
2. loader,plugins优化
- cache-loader 这个loader就是在第一次打包的时候会缓存打包的结果,在第二次打包的时候就会直接读取缓存的内容,从而提高打包效率。但是也需要合理利用,我们要记住一点你加的每一个loader,plugins都会带来额外的打包时间。这个额外时间比他带来的减少时间多,那么一味的增加这个loader就没意义,所以cache-loader最好用在耗时比较大的loader上,配置如下
{
rules: [
{
test: /\.vue$/,
use: [
'cache-loader',
'vue-loader'
],
include: path.resolve(__dirname, './src')
}
]
}
前端性能优化
1. 减少 HTTP 请求
一个完整的 HTTP 请求需要经历 DNS 查找,TCP 握手,浏览器发出 HTTP 请求,服务器接收请求,服务器处理请求并发回响应,浏览器接收响应等过程。
2. 使用服务端渲染
客户端渲染: 获取 HTML 文件,根据需要下载 JavaScript 文件,运行文件,生成 DOM,再渲染。 服务端渲染:服务端返回 HTML 文件,客户端只需解析 HTML。
优点:首屏渲染快,SEO 好。 缺点:配置麻烦,增加了服务器的计算压力。
3. 静态资源使用 CDN
4. 将 CSS 放在文件头部,JavaScript 文件放在底部
所有放在 head 标签里的 CSS 和 JS 文件都会堵塞渲染(CSS 不会阻塞 DOM 解析)。如果这些 CSS 和 JS 需要加载和解析很久的话,那么页面就空白了。所以 JS 文件要放在底部,等 HTML 解析完了再加载 JS 文件。 那为什么 CSS 文件还要放在头部呢? 因为先加载 HTML 再加载 CSS,会让用户第一时间看到的页面是没有样式的、“丑陋”的,为了避免这种情况发生,就要将 CSS 文件放在头部了。 另外,JS 文件也不是不可以放在头部,只要给 script 标签加上 defer 属性就可以了,异步下载,延迟执行。
5. 使用字体图标 iconfont 代替图片图标
字体图标就是将图标制作成一个字体,使用时就跟字体一样,可以设置属性,例如 font-size、color 等等,非常方便。并且字体图标是矢量图,不会失真。还有一个优点是生成的文件特别小。
6. 压缩文件
压缩文件可以减少文件下载时间,让用户体验性更好。 得益于 webpack 和 node 的发展,现在压缩文件已经非常方便了。 在 webpack 可以使用如下插件进行压缩:
- JavaScript:UglifyPlugin
- CSS :MiniCssExtractPlugin
- HTML:HtmlWebpackPlugin
其实,我们还可以做得更好。那就是使用 gzip 压缩。可以通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。当然,服务器也得支持这一功能。 gzip 是目前最流行和最有效的压缩方法。举个例子,我用 Vue 开发的项目构建后生成的 app.js 文件大小为 1.4MB,使用 gzip 压缩后只有 573KB,体积减少了将近 60%。
附上 webpack 和 node 配置 gzip 的使用方法。
- 下载插件
npm install compression-webpack-plugin --save-dev
npm install compression
- webpack 配置
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [new CompressionPlugin()],
}
- node 配置
const compression = require('compression')
// 在其他中间件前使用
app.use(compression())
7. 图片优化
- 1)图片延迟加载 在页面中,先不给图片设置路径,只有当图片出现在浏览器的可视区域时,才去加载真正的图片,这就是延迟加载。对于图片很多的网站来说,一次性加载全部图片,会对用户体验造成很大的影响,所以需要使用图片延迟加载。 首先可以将图片这样设置,在页面不可见时图片不会加载:
<img data-src="https:xxx">
等页面可见时,使用 JS 加载图片:
const img = document.querySelector('img')
img.src = img.dataset.src
这样图片就加载出来了。
- 2) 调整图片大小
例如,你有一个 1920 * 1080 大小的图片,用缩略图的方式展示给用户,并且当用户鼠标悬停在上面时才展示全图。如果用户从未真正将鼠标悬停在缩略图上,则浪费了下载图片的时间。 所以,我们可以用两张图片来实行优化。一开始,只加载缩略图,当用户悬停在图片上时,才加载大图。还有一种办法,即对大图进行延迟加载,在所有元素都加载完成后手动更改大图的 src 进行下载。
- 3) 降低图片质量 例如 JPG 格式的图片,100% 的质量和 90% 质量的通常看不出来区别,尤其是用来当背景图的时候。我经常用 PS 切背景图时, 将图片切成 JPG 格式,并且将它压缩到 60% 的质量,基本上看不出来区别。
压缩方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。
以下附上 webpack 插件 image-webpack-loader 的用法。
npm i -D image-webpack-loader
webpack 配置
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use:[
{
loader: 'url-loader',
options: {
limit: 10000, /* 图片大小小于1000字节限制时会自动转成 base64 码引用*/
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
/*对图片进行压缩*/
{
loader: 'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
- 4) 尽可能利用 CSS3 效果代替图片
有很多图片使用 CSS 效果(渐变、阴影等)就能画出来,这种情况选择 CSS3 效果更好。因为代码大小通常是图片大小的几分之一甚至几十分之一。
8. if-else 对比 switch
当判断条件数量越来越多时,越倾向于使用 switch 而不是 if-else。
if (color == 'blue') {
} else if (color == 'yellow') {
} else if (color == 'white') {
} else if (color == 'black') {
} else if (color == 'green') {
} else if (color == 'orange') {
} else if (color == 'pink') {
}
switch (color) {
case 'blue':
break
case 'yellow':
break
case 'white':
break
case 'black':
break
case 'green':
break
case 'orange':
break
case 'pink':
break
}
像以上这种情况,使用 switch 是最好的。假设 color 的值为 pink,则 if-else 语句要进行 7 次判断,switch 只需要进行一次判断。 从可读性来说,switch 语句也更好。
从使用时机来说,当条件值大于两个的时候,使用 switch 更好。不过 if-else 也有 switch 无法做到的事情,例如有多个判断条件的情况下,无法使用 switch。
- 9) 查找表 当条件语句特别多时,使用 switch 和 if-else 不是最佳的选择,这时不妨试一下查找表。查找表可以使用数组和对象来构建。
switch (index) {
case '0':
return result0
case '1':
return result1
case '2':
return result2
case '3':
return result3
case '4':
return result4
case '5':
return result5
case '6':
return result6
case '7':
return result7
case '8':
return result8
case '9':
return result9
case '10':
return result10
case '11':
return result11
}
可以将这个 switch 语句转换为查找表
const results = [result0,result1,result2,result3,result4,result5,result6,result7,result8,result9,result10,result11]
return results[index]
如果条件语句不是数值而是字符串,可以用对象来建立查找表
const map = {
red: result0,
green: result1,
}
return map[color]
性能优化
- 构建优化: 从
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 进入视口和离开视口