【吐血整理】2022年最新前端面试题大全及答案(收藏)-头条-PHP中文网
1.js的数据类型(关于数据类型相关的)
a.基本数据类型:
Null,undefined,Boolean,Number,String
(1)、ES6新增:Symbol表示独一无二的值;使用symbol作为对象属性名不被Object.key等方式访问;使用JSON.stringify()将对象转换成JSON字符串,Symbol属性会被排除在外;使用Symbol定义类的私有属性/方法
使用JSON.stringify()将对象转换成JSON字符串,Symbol属性会被排除在外;
应用场景:
1、使用Symbol来作为对象属性名(key)
let obj = {
[Symbol('name')]: '一斤代码',
age: 18,
title: 'Engineer'
}
Object.keys(obj) // ['age', 'title']
for (let p in obj) {
console.log(p) // 分别会输出:'age' 和 'title'
}
Object.getOwnPropertyNames(obj) // ['age', 'title']
Symbol类型的key是不能通过Object.keys()
或者for...in
来枚举的,它未被包含在对象自身的属性名集合(property names)之中。所以,利用该特性,我们可以把一些不需要对外操作和访问的属性使用Symbol来定义。
还是会有一些专门针对Symbol的API,比如:
// 使用Object的API
Object.getOwnPropertySymbols(obj) // [Symbol(name)]
// 使用新增的反射API
Reflect.ownKeys(obj) // [Symbol(name), 'age', 'title']
2、使用Symbol来替代常量,保证常量的值是唯一的
3、使用Symbol定义类的私有属性/方法
我们知道在JavaScript中,是没有如Java等面向对象语言的访问控制关键字private的,类上所有定义的属性或方法都是可公开访问的。因此这对我们进行API的设计时造成了一些困扰。
而有了Symbol以及模块化机制,类的私有属性和方法才变成可能。例如:
在文件 a.js中
const PASSWORD = Symbol()
class Login {
constructor(username, password) {
this.username = username
this[PASSWORD] = password
}
checkPassword(pwd) {
return this[PASSWORD] === pwd
}
}
export default Login
在文件 b.js 中
import Login from './a'
const login = new Login('admin', '123456')
login.checkPassword('admin') // true
login.PASSWORD // oh!no!
login[PASSWORD] // oh!no!
login["PASSWORD"] // oh!no!
由于Symbol常量PASSWORD被定义在a.js所在的模块中,外面的模块获取不到这个Symbol,也不可能再创建一个一模一样的Symbol出来(因为Symbol是唯一的),因此这个PASSWORD的Symbol只能被限制在a.js内部使用,所以使用它来定义的类属性是没有办法被模块外访问到的,达到了一个私有化的效果。
(2)、ES10新增:BigInt 表示任意大的整数
BigInt
是一种内置对象,它提供了一种方法来表示大于 253 - 1
的整数。这原本是 Javascript中可以用 Number 表示的最大数字。BigInt
可以表示任意大的整数。
不能用于 Math 对象中的方法;不能和任何 Number 实例混合运算,两者必须转换成同一种类型。在两种类型来回转换时要小心,因为 BigInt
变量在转换成 Number 变量时可能会丢失精度。
使用场景:由于在 Number 与 BigInt
之间进行转换会损失精度,因而建议仅在值可能大于253 时使用 BigInt
类型,并且不在两种类型之间进行相互转换。
b.引用数据类型:
Object、Array、 function、Date、RegExp
c.堆、栈
栈: 是一种连续储存的数据结构,具有先进后出后进先出的性质。
通常的操作有入栈(压栈),出栈和栈顶元素。想要读取栈中的某个元素,就是将其之间的所有元素出栈才能完成。
堆: 是一种非连续的树形储存数据结构,具有队列优先,先进先出; 每个节点有一个值,整棵树是经过排序的。特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。常用来实现优先队列,存取随意。
基本数据类型存放在栈中
基本数据类型是指存放在栈中的简单数据段,数据大小确定,内存空间大小可以分配,它们是直接按值存放的,所以可以直接按值访问
存放在堆内存中的对象,每个空间大小不一样,要根据情况进行特定的配置
引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。
通过这个引用地址可以快速查找到保存在堆内存中的对象
d.null 和 undefined 的区别?
相同:
在 if 语句中 null 和 undefined 都会转为false两者用相等运算符比较也是相等
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
不同:
undefined 代表的含义是未定义,
定义了形参,没有传实参,显示undefined
一般变量声明了但还没有定义的时候会返回 undefined
对象属性名不存在时,显示undefined
函数没有写返回值,即没有写return,拿到的是undefined
null 代表的含义是空对象。也作为对象原型链的终点
null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
e.typeof null的返回值
2.区分数组和对象的方法
a.Array.isArray(变量)
var fruits = ["Banana", "Orange", "Apple", "Mango"];
Array.isArray(fruits) // 结果为:true
var tt={a:1}
Array.isArray(tt) // 结果为:false
b.constructor
对象的constructor属性用于返回创建该对象的函数,也就是我们常说的构造函数。
在JavaScript中,每个具有原型的对象都会自动获得constructor属性。除了arguments、Enumerator、Error、Global、Math、RegExp、Regular Expression等一些特殊对象之外,其他所有的JavaScript内置对象都具备constructor属性。例如:Array、Boolean、Date、Function、Number、Object、String等。
var fruits = ["Banana", "Orange", "Apple", "Mango"];
fruits.constructor // 结果:Array()
var tt={a:1}
tt.constructor // 结果:Object()
c.instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
// 定义构造函数
function C(){}
function D(){}
//实例化对象
var o = new C();
o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上
o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object; // true,同上
C.prototype = {};
var o2 = new C();
o2 instanceof C; // true
o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上.
D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上
d.typeof(不推荐)
对于值的类型,如string/number/boolean我们都可以通过typeof判断,但是typeof在判断引用类型时,返回值只有object/function,你不知道它到底是一个object对象,还是数组,还是new Number等等。
var a = 18;
var b = 'smile';
var c = true;
var d = [];
var e = {};
function f() {
}
console.log(typeof (a));//number
console.log(typeof (b));//string
console.log(typeof (c));//boolean
console.log(typeof (d));//object
console.log(typeof (e));//object
console.log(typeof (f));//function
e.Object.prototype.toString.call
var tt1={}
Object.prototype.toString.call(tt1) // 结果:'[object Object]'
var tt2=[]
Object.prototype.toString.call(tt2) // 结果:'[object Array]'
3.vue常用指令
vue常用指令有:v-cloak指令、v-once指令、v-show指令、v-if指令、v-else指令、v-else-if指令、v-for指令、v-html指令、v-text指令、v-bind指令、v-on指令、v-model指令等等。
1.v-cloak
当网络较慢,网页还在加载 Vue.js ,而导致 Vue 来不及渲染,这时页面就会显示出 Vue 源代码。我们可以使用 v-cloak 指令来解决这一问题。
配合css样式:
[v-cloak]{
display: none;
}
2.v-once
只会执行一次渲染,当数据发生改变时,不会再变化
3.v-show
v-show接受一个表达式或一个布尔值。相当于给元素添加一个
display
属性
4.v-if、v-else、v-else-if
v-if和v-show有同样的效果,不同在于v-if是重新渲染,而v-show使用display属性来控制显示隐藏。频繁切换的话使用v-show减少渲染带来的开销。
说明一下:v-if可以单独使用,而v-else-if,v-else必须与v-if组合使用
v-if、v-else-if都是接受一个条件或布尔值,v-else不需要参数。
5.v-for
v-for可用来遍历数组、对象、字符串。
6.v-text和v-html
v-text
是渲染字符串,会覆盖原先的字符串
v-html
是渲染为html。{{}}
双大括号和v-text
都是输出为文本。那如果想输出为html。
7.v-bind
是用可以将标签内的属性值解析成js代码,在标签的属性中使用v-bind,双引号里的内容会被当作js解析(只能解析变量或三元表达式),如下:
如果给属性值设置为一个变量,那么可以使用v-bind
可以缩写为:<属性>="<变量>"
8.v-on
v-on
用于事件绑定语法:
v-on:<事件类型>="<函数名>"
简写:@<事件类型>="<函数名>"
9.v-model
数据双向绑定指令,限制在
<input>、<select>、<textarea>
、components中使用
4.事件
事件是文档和浏览器窗口中发生的特定的交互瞬间,事件就发生了。
一是直接在标签内直接添加执行语句,
二是定义执行函数。
addeventlistener 监听事件
事件类型分两种:事件捕获、事件冒泡。
事件捕获就是:网景公司提出的事件流叫事件捕获流,由外往内,从事件发生的顶点开始,逐级往下查找,一直到目标元素。
事件冒泡:IE提出的事件流叫做事件冒泡就是由内往外,从具体的目标节点元素触发,逐级向上传递,直到根节点。
什么是事件流?
事件流就是,页面接受事件的先后顺序就形成了事件流。
自定义事件
自定义事件,就是自己定义事件类型,自己定义事件处理函数。
1.事件委托
事件委托,又名事件代理。事件委托就是利用事件冒泡,就是把子元素的事件都绑定到父元素上。如果子元素阻止了事件冒泡,那么委托也就没法实现了
阻止事件冒泡
event.stopPropagation() .stop修饰符
addEventListener(‘click',函数名,true/false) 默认值为false(即 使用事件冒泡)true 事件捕获
好处:提高性能,减少了事件绑定,从而减少内存占用
应用场景 在vue中事件委托:
我们经常遇到vue中v-for一个列表,列表的每一项都绑定了@click处理事件。我们都知道绑定这么多监听,从性能方面来说是不太好的。那我们我们可以通过把每个item的click事件委托给父元素的形式来实现
5.Javascript 的作用域和作用域链
作用域: 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。简单说:函数内部局部作用域,函数外面全局作用域。
作用域就是一个变量可以使用的范围,主要分为全局作用域和函数作用域
全局作用域就是Js中最外层的作用域,在哪里都可以访问
函数作用域是js通过函数创建的一个独立作用域,只能在函数内部访问,函数可以嵌套,所以作用域也可以嵌套
Es6中新增了块级作用域(由大括号包裹,比如:if(){},for(){}等)
6.鼠标事件 mouseenter与mouseover区别
mouseenter: 鼠标进入被绑定事件监听元素节点时触发一次,再次触发是鼠标移出被绑定元素,再次进入时。而当鼠标进入被绑定元素节点触发一次后没有移出,即使鼠标动了也不再触发。
mouseover: 鼠标进入被绑定事件监听元素节点时触发一次,如果目标元素包含子元素,鼠标移出子元素到目标元素上也会触发。
mouseenter 不支持事件冒泡 mouseover 会冒泡
7.数组的方法
1、sort( ),排序
var tt =[1,3,2,6,5]
tt.sort(),结果:[1, 2, 3, 5, 6]
2.reverse( ) ,原数组倒序 它的返回值是倒序之后的原数组
var tt =[1,3,2,6,5]
tt.reverse(),结果:[5, 6, 2, 3, 1]
3. join,讲数组进行分割成为字符串 这能分割一层在套一层就分隔不了了
var tt =[6, 5, 3, 2, 1]
tt.join('-'),结果:'6-5-3-2-1'
4.concat,数组合并
var tt =[6, 5, 3, 2, 1]
var tt2=[8,9]
tt.concat(tt2),结果:[6, 5, 3, 2, 1, 8, 9]
5.toString,数组转字符串
var tt =[6, 5, 3, 2, 1]
tt.toString(),结果:'6,5,3,2,1'
6. shift,从头部删除一个元素 返回被删除掉的元素,改变原有数组
var tt =[6, 5, 3, 2, 1]
tt.shift(),结果:6
7.push,向数组的末尾追加 返回值是添加数据后数组的新长度,改变原有数组
var tt = [8, 5, 3, 2, 1]
tt.push(10),结果:6
tt,结果:[8, 5, 3, 2, 1, 10]
1、sort( ):sort 排序 如果下面参数的正反 控制 升序和降序 ,返回的是从新排序的原数组
2、splice( ):向数组的指定index处插入 返回的是被删除掉的元素的集合,会改变原有数组;截取类 没有参数,返回空数组,原数组不变;一个参数,从该参数表示的索引位开始截取,直至数组结束,返回截取的 数组,原数组改变;两个参数,第一个参数表示开始截取的索引位,第二个参数表示截取的长度,返回截取的 数组,原数组改变;三个或者更多参数,第三个及以后的参数表示要从截取位插入的值。会改变原数据
3、pop( ):从尾部删除一个元素 返回被删除掉的元素,改变原有数组。
4、push( ):向数组的末尾追加 返回值是添加数据后数组的新长度,改变原有数组。
5、unshift( ):向数组的开头添加 返回值是添加数据后数组的新长度,改变原有数组。
6、shift( ):从头部删除一个元素 返回被删除掉的元素,改变原有数组。
7、reverse( ): 原数组倒序 它的返回值是倒序之后的原数组
8、concat( ):数组合并。
9、slice( ):数组元素的截取,返回一个新数组,新数组是截取的元素,可以为负值。从数组中截取,如果不传参,会返回原数组。如果只传入一个参数,会从头部开始删除,直到数组结束,原数组不会改变;传入两个参数,第一个是开始截取的索引,第二个是结束截取的索引,不包含结束截取的这一项,原数组不会改变。最多可以接受两个参数。
10、join( ):讲数组进行分割成为字符串 这能分割一层在套一层就分隔不了了
11、toString( ):数组转字符串;
12、toLocaleString( ):将数组转换为本地数组。
13、forEach( ):数组进行遍历;
14、map( ):没有return时,对数组的遍历。有return时,返回一个新数组,该新数组的元素是经过过滤(逻辑处理)过的函数。
15、filter( ):对数组中的每一运行给定的函数,会返回满足该函数的项组成的数组。
16、every( ):当数组中每一个元素在callback上被返回true时就返回true。(注:every其实类似filter,只不过它的功能是判断是不是数组中的所有元素都符合条件,并且返回的是布尔值)。
17、some( ):当数组中有一个元素在callback上被返回true时就返回true。(注:every其实类似filter,只不过它的功能是判断是不是数组中的所有元素都符合条件,并且返回的是布尔值)。
18、reduce( ):回调函数中有4个参数。prev(之前计算过的值),next(之前计算过的下一个的值),index,arr。把数组列表计算成一个
19.isArray() 判断是否是数组
20. indexOf 找索如果找到了就会返回当前的一个下标,若果没找到就会反回-1
21. lastIndexOf 它是从最后一个值向前查找的 找索如果找到了就会返回当前的一个下标,若果没找到就会反回-1
22. Array.of() 填充单个值
23. Array.from() 来源是类数组
24.fill填充方法 可以传入3各参数 可以填充数组里的值也就是替换 如果一个值全部都替换掉 , 第一个参数就是值 第二个参数 从起始第几个 第三个参数就是最后一个
find 查找这一组数 符合条件的第一个数 给他返回出来
findIndex() 查找这一组数 符合条件的第一数的下标 给他返回出来 没有返回 -1
keys 属性名 values属性值 entries属性和属性值
forEach 循环遍历 有3个参数 无法使用 break continue , 参数一就是每个元素 参数二就是每个下标 参数三就是每个一项包扩下标和元素
改变数组本身的api
1. `pop()` 尾部弹出一个元素
2. `push()` 尾部插入一个元素
3. `shift()` 头部弹出一个元素
4. `unshift()` 头部插入一个元素
5. `sort([func])` 对数组进行排序,func有2各参数,其返回值小于0,那么参数1被排列到参数2之前,反之参数2排在参数1之前
6. `reverse()` 原位反转数组中的元素
7. `splice(pos,deleteCount,...item)` 返回修改后的数组,从pos开始删除deleteCount个元素,并在当前位置插入items
8. `copyWithin(pos[, start[, end]])` 复制从start到end(不包括end)的元素,到pos开始的索引,返回改变后的数组,浅拷贝
9. `arr.fill(value[, start[, end]])` 从start到end默认到数组最后一个位置,不包括end,填充val,返回填充后的数组
其他数组api不改变原数组
8.this指向的问题(高频)
在全局的环境下this是指向window 的
普通函数调用直接调用中的this 会指向 window, 严格模式下this会指向 undefined,自执行函数 this 指向 window,定时器中的 this 指向 window
在对象里调用的this,指向调用函数的那个对象,
在构造函数以及类中的this,构造函数配合 new 使用, 而 new 关键字会将构造函数中的 this 指向实例化对象,所以构造函数中的 this 指向 当前实例化的对象
方法中的this谁调用就指向谁。
箭头函数没有自己的 this,箭头函数的this在定义的时候,会继承自外层第一个普通函数的this
9. 什么是深拷贝,浅拷贝,浅拷贝 赋值的区别,如何实现
深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。
1.浅拷贝:
将原对象或原数组的引用直接赋给新对象,新数组,新对象只是对原对象的一个引用,而不复制对象本身,新旧对象还是共享同一块内存
如果属性是一个基本数据类型,拷贝就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,
2.深拷贝:
创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”
深拷贝就是把一个对象,从内存中完整的拷贝出来,从堆内存中开辟了新区域,用来存新对象,并且修改新对象不会影响原对象
3、赋值:
当我们把一个对象赋值给一个新的变量时,赋的是该对象在栈中的内存地址,而不是堆中的数据。也就是两个对象
【...arr】,解构赋值:针对一维数组和对象可以看做是深拷贝
常见的实现深拷贝的方法(80%):JSON.parse(JSON.stringify(val))
不常见的实现深拷贝的方法(20%):像function这种的上面这种方法是拷贝不了的
function=>"function"
function deepClone(source){
const targetObj = source.constructor === Array?[]:{}
for(let keys in source){
if(source.hasOwnProperty(keys)) {
// 引用数据类型
if(source[keys]&&typeof source[keys] === 'object'){
//targetObj[keys]=targetObj[keys].constructor === Array?[]:{}
//递归
targetObj[keys]=deepClone(targetObj[keys])
} else { // 基本数据类型
targetObj[keys]==source[keys]
}
}
}
}
10.arguments 的对象是什么?
arguments 当我们不知道有多少个参数传进来的时候就用 arguments 来接收,是一个类似于数组的对象,他有length属性,可以arguments[ i ]来访问对象中的元素, 但是它不能用数组的一些方法。 例如push、pop、slice等。arguments虽然不是一个数组,但是它可以转成一个真正的数组。
取之可以用 展开运算符来 数组和类数组类数组: ①拥有length属性,其它属性(索引)为非负整数;箭头函数里没有arguments ②不具有数组所具有的方法; ③类数组是一个普通对象,而真实的数组是Array类型。
常见的类数组:arguments,document.querySelectorAll得到的列表,jQuery对象($("div"));
11.什么是高阶函数?
<script>
//函数add就是个高阶函数
function add(x,y,fun) {
return fun(x)+fun(y)
}
function fun(x){
return Math.abs(x)
}
console.log(add(-2,3,fun)) // 输出5
</script>
常见的高阶函数: Map、reduce、sort、filter
举例:
12.常见的跨域方式有哪些?
同源策略:域名、端口、协议----》同源
跨域:违反同源策略
常见的方式:
1. jsonp
利用html中script标签的src属性来获取其他源的数据
2.cors跨域资源共享 支持所有的主流浏览器 ie9+
原理:使用XMLHttpRequest发送请求的时候,如果不同源,heaters{origin}
后台处理:Acces-control-allow-origin:
3.h5中的window.postMessage跨域 支持主流浏览器 ie8+
window.postMessage("字符串","*")
我们还需要说下在各个框架里面的一些跨域
例如: vue跨域
......不止以上4种,但回答出以上4种基本上8、9分是有了
13.谈谈你对http2.0的理解
我们可以打开网站的network面板,现在除了一些特别老的政府、教育类网站还会是http1.1,市面上主流的大厂基本都是用的http2.0
14.垂直水平居中的方法
1.margin-block
**
margin-block
**这个CSS属性定义了一个元素的逻辑块开始和结束边距,根据元素的写入模式、方向性和文本方向映射到物理边界。/* 有长度的具体的值 */ margin-block: 10px 20px; /* 一个绝对的长度值 */ margin-block: 1em 2em; /* 相对于文本大小的值 */ margin-block: 5% 2%; /* 相对于最近的块容器宽度的值 */ margin-block: 10px; /* 设置开始值和结束值 */ /* 关键字 值 */ margin-block: auto;
效果图+关键代码如下所示:
2.绝对定位+transform
body {
position: relative;
height: 100vh;
}
.tt {
width: 200px;
height: 200px;
background-color: pink;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
3.绝对定位+margin
body {
position: relative;
height: 100vh;
}
.tt {
width: 200px;
height: 200px;
background-color: pink;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
margin: auto;
}
4.flex布局
body {
position: relative;
height: 100vh;
display: flex;
align-items: center;/*垂直居中*/
justify-content: center;/*水平居中*/
}
.tt {
width: 200px;
height: 200px;
background-color: pink;
}
15.webpack常用plugins
webpack中的plugin
,赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 webpack 的不同阶段(钩子 / 生命周期),贯穿了webpack整个编译周期;目的在于解决loader 无法实现的其他事。
1.CleanWebpackPlugin
每次修改一些配置,重新打包时,都需要 手动删除dist文件
我们可以借助 CleanWebpackPlugin 插件来帮我们完成
npm install clean-webpack-plugin -D
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
module.exports = {
// 其他省略
plugins: [
new CleanWebpackPlugin()
]
}
2.HtmlWebpackPlugin
以一个html文件为模板,生成一个html文件,并将打包生成的js文件注入当中, 在多个入口文件的情况下还可以自定义引入js文件
安装
npm install html-webpack-plugin -D
配置
publuc/index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
webpack.config.js 配置
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
// 这里三个入口是为了解释 HtmlWebpackPlugin 的 chunks 及 excludeChunks
entry: {
page1: "./src/page1.js",
page2: "./src/page2.js",
common: "./src/common.js"
},
plugins: [
new HtmlWebpackPlugin({
filename: "index.html", // 生成的 html 的文件名
template: "index.template.html", // 指定模板
title: "hello webpack", // 设置 html 的 title,可以在 html 中通过 ejs 语法引入
inject: true, // 默认值,script标签位于 body 底部,可选值 body、header、false(表示不自动引入js)
hash: false, // true 表示引入的js文件后面添加 hash 值作为参数,src="main.js?78ccc964740f25e35fca"
chunks: [page1, common], // 多入口打包会有多个文件,默认引入全部,此配置表示只引入 page1, common
minify: {
collapseWhitespace: true, // 去除空格
minifyCSS: true, // 压缩 html 内联的css
minifyJS: true, // 压缩 html 内联的js
removeComments: true, // 移除注释
}
}),
// 多页面需要 new 多个对象
new HtmlWebpackPlugin({
...
excludeChunk: [page1], // 不需要引入page1,即只引入 page2 与
})
]
}
title: 生成的html文档的标题。
配置该项,它并不会替换指定模板文件中的title元素的内容,除非html模板文件中使用了模板引擎语法来获取该配置项值,如下ejs模板语法形式:
<title>{%= o.htmlWebpackPlugin.options.title %}</title>
filename:输出文件的文件名称,默认为index.html,不配置就是该文件名;此外,还可以为输出文件指定目录位置(例如’html/index.html’)
template: 本地模板文件的位置
inject:向template或者templateContent中注入所有静态资源,不同的配置值注入的位置不经相同。
1、true或者body:所有JavaScript资源插入到body元素的底部
2、head: 所有JavaScript资源插入到head元素中
3、false: 所有静态资源css和JavaScript都不会注入到模板文件中
chunks:允许插入到模板中的一些chunk,不配置此项默认会将entry中所有的thunk注入到模板中。在配置多个页面时,每个页面注入的thunk应该是不相同的,需要通过该配置为不同页面注入不同的thunk;
excludeChunks: 这个与chunks配置项正好相反,用来配置不允许注入的thunk。
3.uglifyjs-webpack-plugin
压缩、美化js,去除代码中的console代码...
安装
npm install uglifyjs-webpack-plugin -D
使用方式如下:
在webpack.config.js中新增以下代码
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
optimization: {
minimize: true,
minimizer: [
new UglifyJSPlugin({ // 此配置生效的条件mode: 'production',如果是development,则以下压缩js的代码不会生效的
uglifyOptions: {
compress: {
drop_console: true,//注释代码中的console代码
drop_debugger: true,//注释代码中的debugger代码
pure_funcs: ['console.log', 'console.info'], // 删除console.log+console.info的代码
},
},
})
],
},
备注:这个 uglifyjs-webpack-plugin用起来要注意mode的值,经试验配置生效的条件mode: 'production',如果是development,则以下压缩js的代码不会生效的
4.terser-webpack-plugin
功能与uglifyjs-webpack-plugin非常相似
5.BannerPlugin
为每个 chunk 文件头部添加 banner,
该插件是webpack内置的,主要用于对打包好的js文件的最开始处添加版权声明等信息
配置方式如下:
效果图如下:
16.npm相关命令
npm install name -save-dev 简写(npm install name -D)开发依赖,自动把模块和版本号添加到devdependencies;这些包只在做项目的时候会使用到,在项目打包上线后不依赖于这些包项目依然可以正常运行。比如:gulp/webpack、eslint、sass等等。
npm install name -save 简写(npm install name -S)生产依赖,自动把模块和版本号添加到dependencies。会把包添加到package.json的dependencies下,这些包在项目打包上线后依然需要使用项目才能正常运行,比如:axios、element-ui、vue-router等等。
17.webpack入口、出口文件配置
1.一个入口(entry)一个出口(output)
代码结构如下所示:
2.多个entry和output配置
// 多入口(首页和other页)
entry: {
home: './src/home.js',
other: './src/other.js',
},
output: {
// name代表home或者other
filename: '[name].js', // 如果这里还写filename: "bundle.js" -- error:两个出口
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin({
template: __dirname + "/index.html",
filename: 'index.html',
chunks:['home'] //代表:以index.html为模板打包出来的html页面只会引入home.js
}),
new HtmlWebpackPlugin({
template: __dirname + "/index.html",
filename: 'other.html',
chunks:['home','other']//代表:以index.html为模板打包出来的html页面会引入home.js和other.js
})
],
18.webpack引入css文件
使用传统的在html文件中引入css的方式在编译后报报css加载失败
传统方式如下:
<link rel="stylesheet" href="./index.css" type="text/css"/>
引入方式:
1.安装相关loader
npm install css-loader --save-dev
npm install style-loader --save-dev
2.在webpack.config.js中进行相关配置
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
}
],
},
],
},
3.在入口js中用require的方式引入
require("../src/index.css")
19.webpack配置devServer本地运行
版本:
"webpack": "^5.74.0"
"webpack-dev-server": "^4.11.1"
1.配置运行脚本
"webpack-dev-server": "^4.11.1"
2.webpack.config.dev.js文件代码如下:
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
//开发服务器devServer:用来自动化(自动编译,自动打开浏览器,自动刷新浏览器~~)
//特点:只会在内存中编译打包,不会有任何输出
// 利用 gzips 压缩 public/ 目录当中的所有内容并提供一个本地服务(serve)
devServer: {
static: {
// 运行代码的目录
directory: path.join(__dirname, "public"),
},
compress: true,
// 端口号
port: 9000,
// 自动打开浏览器
open: true
},
entry : './src/index.js', // 入口
output:{// 出口文件(打包后生成的文件)
path: path.resolve(__dirname,'public'), // 执行打包命令后,项目会自动生成一个public文件夹,并把打包后的代码放在这
filename: 'bundle.js'
},
mode: 'development',
module: {
rules: [
{
test: /\.css$/i,
use: ['style-loader', 'css-loader'],
},
{
test: /\.(png|jpg|gif)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192,
},
},
],
},
],
},
optimization: {
minimize: true,
minimizer: [
new UglifyJSPlugin({ // 此配置生效的条件mode: 'production',如果是development,则以下压缩js的代码不会生效的
uglifyOptions: {
compress: {
drop_console: true,//注释代码中的console代码
drop_debugger: true,//注释代码中的debugger代码
pure_funcs: ['console.log', 'console.info'], // 删除console.log+console.info的代码
},
},
})
],
},
plugins:[
new webpack.BannerPlugin('made in tt'),
new HtmlWebpackPlugin({
template: __dirname + '/src/index.html'
})
]
}
20.常见的前端设计模式
1.外观模式
外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API。很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已
兼容浏览器事件绑定
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
封装接口
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
场景
设计初期,应该要有意识地将不同的两个层分离,比如经典的三层结构,在数据访问层和业务逻辑层、业务逻辑层和表示层之间建立外观Facade
在开发阶段,子系统往往因为不断的重构演化而变得越来越复杂,增加外观Facade可以提供一个简单的接口,减少他们之间的依赖。
在维护一个遗留的大型系统时,可能这个系统已经很难维护了,这时候使用外观Facade也是非常合适的,为系系统开发一个外观Facade类,为设计粗糙和高度复杂的遗留代码提供比较清晰的接口,让新系统和Facade对象交互,Facade与遗留代码交互所有的复杂工作。
优点
减少系统相互依赖。
提高灵活性。
提高了安全性
缺点
不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
2.工厂模式
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
3.单例模式
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
1、单例类只能有一个实例。2、单例类必须自己创建自己的唯一实例。3、单例类必须给所有其他对象提供这一实例。
意图:
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:
一个全局使用的类频繁地创建与销毁。
何时使用:
当您想控制实例数目,节省系统资源的时候。
如何解决:
判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:
构造函数是私有的。
优点:
1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
2、避免对资源的多重占用。
前端使用单例模式的场景
1.登录框和遮罩层,整个系统只需要实例化一次,不需要每次需要显示的时候都重新实例化一下
代码演示:
// 登录框
// 全局只实例化一次 而且是在类内实例化 不在外部实例化
class LoginForm {
// 只允许在内部实例化
private constructor() { }
// 控制显示状态
private state: string = 'hide' // hide | show
// 是否实例的标志
private static instance: LoginForm | null = null
// 单例模式
static getInstance() {
// 判断系统是否已经有单例了
if (LoginForm.instance === null) {
LoginForm.instance = new LoginForm()
}
return LoginForm.instance
}
show(){
if(this.state === 'show') {
console.log('已经显示了');
return
}
console.log('显示 LoginForm')
// 处理逻辑
this.state = 'show'
}
hide(){
if(this.state === 'hide') {
console.log('已经隐藏了');
return
}
console.log('隐藏 LoginForm')
// 处理逻辑
this.state = 'hide'
}
}
const LoginForm1 = LoginForm.getInstance()
const LoginForm2 = LoginForm.getInstance()
console.log(LoginForm1 === LoginForm2);
2.Vuex 的 store 全局只有一个
3.自定义事件全局只有一个
4.观察者模式
又称发布-订阅模式(Publish/Subscribe Pattern),是我们经常接触到的设计模式,日常生活中的应用也比比皆是,比如你订阅了某个博主的频道,当有内容更新时会收到推送;又比如JavaScript中的事件订阅响应机制。观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。
场景
- DOM事件
document.body.addEventListener('click', function() {
console.log('hello world!');
});
document.body.click()
- vue 响应式
优点
支持简单的广播通信,自动通知所有已经订阅过的对象
目标对象与观察者之间的抽象耦合关系能单独扩展以及重用
增加了灵活性
观察者模式所做的工作就是在解耦,让耦合的双方都依赖于抽象,而不是依赖于具体。从而使得各自的变化都不会影响到另一边的变化。
缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解
21.vue computed与watch的区别
1.computed计算属性
- 如果函数所依赖的属性没有发生变化,从缓存中读取
- 必须有return返回
- 使用方法和data中的数据一样,但是类似一个执行方法
2.watch监听器
- watch的函数名必须和data中的数据名一致
- watch中的函数有俩个参数,新旧
- watch中的函数是不需要调用的
- 只会监听数据的值是否发生改变,而不会去监听数据的地址是否发生改变,要深度监听需要配合deep:true属性使用
- immediate:true 页面首次加载的时候做一次监听
3.二者之间的区别
功能:computed是计算属性,watch是监听一个值的变化而执行对应的回调
是否调用缓存:computed函数所依赖的属性不变的时候会调用缓存;watch每次监听的值发生变化时候都会调用回调
是否调用return:computed必须有;watch可以没有
使用场景:computed当一个属性受多个属性影响的时候;例如购物车商品结算;watch当一条数据影响多条数据的时候,例如搜索框
是否支持异步:computed函数不能有异步;watch可以
22.promise
1. 概念
promise是ES6为解决异步回调而生,是异步编程的一种解决方案,为了解决回调地狱;从语法上说,promise是一种构造函数。
三种状态:
pending(进行中,初始状态)
fulfilled(成功状态,也叫resolved)
rejected(失败状态
Promise只有两个过程(状态):
(1)pending -> fulfilled : Resolved(已完成)
(1)pending -> rejected:Rejected(已拒绝)
通过函数resolve将状态转为fulfilled,函数reject将状态转为rejected,状态一经改变就不可以再次变化。
一个 promise 对象只能改变一次, 无论变为成功还是失败, 都会有一个结果数据 ,成功的结果数据一般称为 value, 失败的结果数据一般称为 reason。
1)什么是回调地狱?
回调函数嵌套使用,外部回调函数异步执行的结果是嵌套的回调执行条件。具体来说就是异步返回值又依赖于另一个异步返回值(看下面的代码)
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result:' + finalResult)
}, failureCallback)
}, failureCallback)
}, failureCallback)
2. Promise的状态可变吗?
一经改变不能再改变。
3.Promise的缺点
缺点:
(1)无法取消Promise,一旦新建它就会立即执行,无法中途取消。
(2)如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
(3)当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
4.Promise.all 方法:Promise.all(iterable)
Promise.all 方法:Promise.all(iterable)
说明:返回一个新的 promise,只有所有的 promise 都成功才成功,只要有一个失败了就直接失败。
let p1= new Promise((resolve,reject)=>{
resolve("success")
})
let p2 = Promise.resolve('success')
let p3 = Promise.resolve('yet')
let p4 = Promise.reject('error')
const res = Promise.all([p1,p2,p3]);
res.then(
value => {
console.log('res成功的回调', value)
},
reason => {
console.log('res失败的回调', reason)
}
)
const res2 = Promise.all([p1,p2,p3,p4]);
res2.then(//p4失败,所以res2失败
value => {
console.log('res2成功的回调', value)
},
reason => {
console.log('res2失败的回调', reason)
}
)let p1= new Promise((resolve,reject)=>{
resolve("success")
})
let p2 = Promise.resolve('success')
let p3 = Promise.resolve('yet')
let p4 = Promise.reject('error')
const res = Promise.all([p1,p2,p3]);
const res2 = Promise.all([p1,p2,p3,p4]);
console.log(res)
console.log(res2)
5.如何改变 promise 的状态?
(1)resolve(value):如果当前是 pending 就会变为 resolved
(2)reject(reason):如果当前是 pending 就会变为 rejected
(3)抛出异常:如果当前是 pending 就会变为 rejected
6.如何让Promise顺序执行?应用场景是什么?
const p1 = new Promise(resolve => {
setTimeout(()=> {
resolve(1);
}, 3000);
});
const p2 = new Promise(resolve => {
setTimeout(()=> {
resolve(2);
}, 2000);
});
const p3 = new Promise(resolve => {
setTimeout(()=> {
resolve(3);
}, 1000);
});
async function execute() {
await p1.then(data => console.log(data));
await p2.then(data => console.log(data));
await p3.then(data => console.log(data));
};
execute();
//1
//2
//3
7.async 和 promise 的区别
1、Async/Await 代码看起来简洁一些,使得异步代码看起来像同步代码
2、async await与Promise一样,是非阻塞的。
3、async await是基于Promise实现的,可以说是改良版的Promise,它不能用于普通的回调函数。
23、sleep
sleep是一种函数,他的作用是使程序暂停指定的时间,起到延时的效果。
官方介绍:sleep是一种函数,作用是延时,程序暂停若干时间,在执行时要抛出一个中断异常,必须对其进行捕获并处理才可以使用这个函数。js中是没有sleep方法的,需要自己定义。
基于async函数的sleep
function sleep(time){
return new Promise((resolve) => setTimeout(resolve, time));
}
async function run(){
console.time('runTime:');
console.log('1');
await sleep(2000);
console.log('2');
await sleep(1000);
console.log('3');
console.timeEnd('runTime:');
}
run();
console.log('a');
// 1
// a
// 2
// 3
// runTime:: 3009.984ms
24.微信小程序setDat的工作原理
在小程序中各个页面之间是相互独立的,一个页面分为渲染层(视图层 webview),逻辑层(JavaScript),系统层(底层)
在架构上,WebView和 JavascriptCore 都是独立的模块,并不具备数据直接共享的通道
换而言之,若要将逻辑层中的data的数据渲染到页面中,他们之间是无法直接通信的,往往需要系统层作为中间角色。
从上面的这张图中就可以看到,当逻辑层data数据渲染到界面的时候,逻辑层的数据需要经过系统层,当系统层接收到这个逻辑层的数据后
系统层在把数据转发给渲染层,然后在渲染层展示出来,在这个过程当中是异步的
视图层和逻辑层的数据传输,实际上通过两边提供的 JavScript Core所实现,即用户传输的数据,需要将其转换为字符串形式传递
同时把转换后的数据内容拼接成一份 JS 脚本,再通过执行 JS 脚本的形式传递到两边独立的环境
setData两个重要的参数
从官方文档中看到这句Page.prototype.setData(Object data, Function callback),得知,setData方法是挂载当前页面实例Page原型下一个公用实例方法
也就是说,Page 下面的任何一个方法内,都可以使用 setData 方法,它接收两个参数
一个是Object data,第一个参数Object data是必传的,数据类型是Object,所代表的含义是,这次要改变的数据
而第二个参数Function callback回调函数是非必填的,它所代表的含义是,setData引起的界面更新渲染完毕后的回调函数
在Web 开发中,开发者使用JavaScript通过Dom接口来完成界面的实时更新。而在小程序中,使用WXML语言所提供的数据绑定功能,来完成此项功能
在小程序中是没有DOM,BOM的那一套东西的,没有document.getElementById等的
小程序是数据驱动视图的,逻辑层中的 data 数据改变了,视图层 view 也会跟着改变,它是单向数据流的,如果想要触发视图中数据的更新,那么就需要借助setData这个方法
上面的WXML通过{{变量名}}来绑定 WXML文件和对应的JavaScript文件中的data对象属性
setData注意事项
- 直接修改 this.data,而不调用this.setData是无法改变页面的状态的,还会造成数据不一致
- 仅支持设置可JSON化的数据,如果不是 JSON 对象数据格式,需要将数据进行转化成json对象`,key:value形式
- 单次设置的数据不能超过1024kB(1M),不要一次设置过多的数据(由于小程序运行逻辑线程与渲染线程之上,setData的调用会把数据从逻辑层传到渲染层,数据太大会增加通信时间,会增加脚本的编译执行时间,占用 WebView JS 线程,)
- 不要把 data中任何一项的value设为undefined,否则这一项将不被设置并可能遗留一些潜在问题
- 页面中需要显示的数据,可以挂载在data下面初始化,虽然这个值不一定要先设置,但是建议先声明然后在使用
- 避免setData的调用过于频繁(setData接口的调用涉及逻辑层与渲染层间的线程通信,通信过于频繁可能导致处理队列阻塞,界面渲染不及时而导致卡顿,应避免无用的频繁调用)
- 在Android下用户在滑动时会感觉到卡顿,操作反馈延迟严重,因为JS线程一直在编译执行渲染,未能及时将用户操作事件传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层
- 渲染有出现延时,由于WebView的 JS 线程一直处于忙碌状态,所以,逻辑层到页面层的通信耗时上升,视图层收到的数据消息时距离发出时间已经过去了几百毫秒,渲染的结果并不是实时的
- 避免 setData 数据冗余(setData操作会引起框架处理一些渲染界面相关的工作,避免将未绑定在 WXML 的变量传入setData,减少不必要的性能消耗)
- 后台态页面进行setData(比如退出小程序),当页面进入后台态(用户不可见),不应该继续去进行 setData,后台态页面的渲染用户是无法感受到的,另外后台态页面去 setData 也会抢占前台页面的执行
25.原生 JavaScript发送GET 、POST请求方法
// get 请求接口
function fooGet() {
let url = "http://www.dafei.com/api/testGet.php?name=dafei"
//第一步:建立所需的对象
let httpRequest = new XMLHttpRequest();
//第二步:打开连接 将请求参数写在url中
httpRequest.open('GET', url, true);
//第三步:发送请求 将请求参数写在URL中
httpRequest.send();
httpRequest.onreadystatechange = function () {
if (httpRequest.readyState === 4 && httpRequest.status === 200) {
alert("get接口请求成功 ok")
var json = httpRequest.responseText; //获取到json字符串,还需解析
console.log(json);
}
};
}
// post 请求接口
function fooPost() {
let url = "http://www.dafei.com/api/testPost.php"
//第一步:创建需要的对象
var httpRequest = new XMLHttpRequest();
//第二步:打开连接 /***发送json格式文件必须设置请求头 ;如下 - */
httpRequest.open('POST', url, true);
//设置请求头 注:post方式必须设置请求头(在建立连接后设置请求头)
httpRequest.setRequestHeader("Content-type", "application/json");
const obj = {
foo: "123", bar: "456", name: "dafei"
}
httpRequest.send(JSON.stringify(obj)); //发送请求 将json写入send中sss
httpRequest.onreadystatechange = function () {//请求后的回调接口,可将请求成功后要执行的程序写在其中
if (httpRequest.readyState === 4 && httpRequest.status === 200) {//验证请求是否发送成功
alert("post接口请求成功 ok")
var json = httpRequest.responseText;//获取到服务端返回的数据
console.log(json);
}
};
}
26.uni-app编译原理
uni-app代码编写,基本语言包括js、vue、css。以及ts、scss等css预编译器。
uni-app分
编译器
和运行时(runtime)
。uni-app能实现一套代码、多端运行,是通过这2部分配合完成的。
编译器将开发者的代码进行编译,编译的输出物由各个终端的runtime进行解析,每个平台(Web、Android App、iOS App、各家小程序)都有各自的runtime。
- 开发者按uni-app规范编写代码,由编译器将开发者的代码编译生成每个平台支持的特有代码
- 在web平台,将.vue文件编译为js代码。与普通的vue cli项目类似。
- 在微信小程序平台,编译器将.vue文件拆分生成wxml、wxss、js等代码
- 在app平台,将.vue文件编译为js代码。进一步,如果涉及uts代码:
- 在iOS平台,将.uts文件编译为swift代码
- 在Android平台,将.uts文件编译为kotlin代码
27.微信小程序优化方案
1.代码包体积优化
启动性能优化最直接的手段是降低代码包大小,代码包大小直接影响了下载耗时,影响用户启动小程序时的体验。
开发者可以采取以下手段优化代码包体积:
1. 合理使用分包加载
建议开发者按照功能划分,将小程序的页面按使用频率和场景拆分成不同分包,实现代码包的按需加载。
小程序单个代码包的体积上限为 2M,使用分包可以提升小程序代码包总体积上限,承载更多的功能与服务。(最新版的微信开发者工具好像已经扩展到5M了)
小程序编译时会将所有 js 文件打包成同一个文件一次性的注入,并执行所有页面和自定义组件的代码。分包后可以降低注入和实际执行的代码量,从而降低注入耗时。
此外,结合分包加载的几个扩展功能,可以进一步优化启动耗时:
1.1 独立分包
小程序中的某些场景(如广告页、活动页、支付页等),通常功能不是很复杂且相对独立,对启动性能有很高的要求。独立分包可以独立于主包和其他分包运行。从独立分包页面进入小程序时,不需要下载主包。建议开发者将部分对启动性能要求很高的页面放到特殊的独立分包中。
1.2 分包预下载
在使用「分包加载」后,虽然能够显著提升小程序的启动速度,但是当用户在使用小程序过程中跳转到分包内页面时,需要等待分包下载完成后才能进入页面,造成页面切换的延迟,影响小程序的使用体验。分包预下载便是为了解决首次进入分包页面时的延迟问题而设计的。
独立分包和分包预下载可以配合使用,获得更好的效果,详情请参考独立分包与分包预下载教程
1.3 分包异步化
「分包异步化」将小程序的分包从页面粒度细化到组件甚至文件粒度。这使得本来只能放在主包内页面的部分插件、组件和代码逻辑可以剥离到分包中,并在运行时异步加载,从而进一步降低启动所需的包大小和代码量。
分包异步化能有效解决主包大小过度膨胀的问题。
2. 避免非必要的全局自定义组件和插件
在 app.json
中通过 usingComponents
全局引用的自定义组件和通过 plugins
全局引入的插件,会在小程序启动时随主包一起下载和注入 JS 代码,影响启动耗时。
即使扩展库和部分官方插件不占用主包大小,但是启动时仍然需要下载和注入 JS 代码,对启动耗时的影响和其他插件并没有区别。
- 如果自定义组件只在某个分包的页面中使用,应定义在页面的配置文件中
- 全局引入的自定义组件会被认为是所有分包、所有页面都需要的,会影响「按需注入」的效果和小程序代码注入的耗时。
- 如果插件只在某个分包的中使用,请仅在分包中引用插件
3. 控制代码包内的资源文件
小程序代码包在下载时会使用 ZSTD 算法进行压缩,图片、音频、视频、字体等资源文件会占用较多代码包体积,并且通常难以进一步被压缩,对于下载耗时的影响比代码文件大得多。
建议开发者在代码包内的图片一般应只包含一些体积较小的图标,避免在代码包中包含或在 WXSS 中使用 base64 内联过多、过大的图片等资源文件。这类文件应尽可能部署到 CDN,并使用 URL 引入。
4. 及时清理无用代码和资源
除了工具默认忽略或开发者明确声明忽略的文件外,小程序打包会将工程目录下所有文件都打入代码包内。意外引入的第三方库、版本迭代中被废弃的代码或依赖、产品环境不需要的测试代码、未使用的组件、插件、扩展库,这些没有被实际使用到的文件和资源也会被打入到代码包里,从而影响到代码包的大小。
建议使用微信开发者工具提供的「代码静态依赖分析」,不定期地分析代码包的文件构成和依赖关系,以此优化代码包大小和内容。对于仅用于本地开发调试,不应包含在小程序代码包的文件,可以使用工具设置的 packOptions.ignore 配置忽略规则。
在使用打包工具(如 Webpack、Rollup 等)对小程序代码进行预处理时,可以利用 tree-shaking 等特性去除冗余代码,也要注意防止打包时引入不需要的库和依赖。
2.代码注入优化
1. 启动过程中减少同步 API 的调用
在小程序启动流程中,会注入开发者代码并顺序同步执行
App.onLaunch
,App.onShow
,Page.onLoad
,Page.onShow
。在小程序初始化代码(Page,App 定义之外的内容)和上述启动相关的几个生命周期中,应尽量减少或不调用同步 API。绝大多数同步 API 会以
Sync
结尾,但有部分特例,比如getSystemInfo
。同步 API 虽然使用起来更简单,但是会阻塞当前 JS 线程,影响代码执行。如非必要,应尽可能的使用异步 API 代替同步,并将启动过程中非必要的同步 API 调用延迟到启动完成后进行。
常见的开发者容易在启动时过于频繁调用的 API 有:
3.1 getSystemInfo/getSystemInfoSync
由于历史原因,这两个接口都是同步实现。由于 getSystemInfo 接口里承载了过多内容,单次调用可能比较久。
如非必要,建议开发者对调用结果进行缓存,避免重复调用。启动过程中应尽可能最多调用一次。
建议优先使用拆分后的 getSystemSetting/getAppAuthorizeSetting/getDeviceInfo/getWindowInfo/getAppBaseInfo 按需获取信息,或使用使用异步版本 getSystemInfoAsync。
3.2 getStorageSync/setStorageSync
getStorageSync/setStorageSync 应只用来进行数据的持久化存储,不应用于运行时的数据传递或全局状态管理。启动过程中过多的读写存储,也会显著影响小程序代码注入的耗时。
对于简单的数据共享,可以使用在 App 上增加全局数据对象完成
2.避免启动过程进行复杂运算
在小程序初始化代码(Page,App 定义之外的内容)和启动相关的几个生命周期中,应避免执行复杂的运算逻辑。复杂运算也会阻塞当前 JS 线程,影响启动耗时。建议将复杂的运算延迟到启动完成后进行。
3.首屏渲染优化
1. 使用「按需注入」和「用时注入」
2. 启用「初始渲染缓存」
自基础库版本 2.11.1 起,小程序支持启用初始渲染缓存。开启后,可以在非首次启动时,使视图层不需要等待逻辑层初始化完毕,而直接提前将页面渲染结果展示给用户,这可以使「首页渲染完成」和页面对用户可见的时间大大提前。
3. 避免引用未使用的自定义组件
在页面渲染时,会初始化在当前页面配置和全局配置通过 usingComponents
引用的自定义组件,以及组件所依赖的其他自定义组件。未使用的自定义组件会影响渲染耗时。
当组件不被使用时,应及时从 usingComponents
中移除。
4. 精简首屏数据
首页渲染的耗时与页面的复杂程度正相关。对于复杂页面,可以选择进行渐进式的渲染,根据页面内容优先级,优先展示页面的关键部分,对于非关键部分或者不可见的部分可以延迟更新。
此外,与视图层渲染无关的数据应尽量不要放在 data 中,避免影响页面渲染时间。
5. 提前首屏数据请求
很多小程序在渲染首页时,需要依赖服务端的接口数据(如商品列表等),此时小程序的首页可能是空白或者骨架屏。
由于网络请求需要相对较长的时间,我们建议开发者在 Page.onLoad
或更早的时机发起网络请求,而不应等待 Page.onReady
之后再进行。
为了进一步提前请求发起的时机,小程序为开发者提供了以下能力:
- 数据预拉取:能够在小程序冷启动时,由微信客户端通过微信后台提前向第三方服务器拉取业务数据,当代码包加载完时可以更快地渲染页面,减少用户等待时间。
- 周期性更新:在用户未打开小程序的情况下,也能从服务器提前拉取数据,当用户打开小程序时可以更快地渲染页面,减少用户等待时间。
6. 缓存请求数据
小程序提供了wx.setStorage、wx.getStorage等读写本地缓存的能力,数据存储在本地,返回的会比网络请求快。如果开发者基于某些原因无法采用数据预拉取与周期性更新,我们推荐优先从缓存中获取数据来渲染视图,等待网络请求返回后进行更新。
7. 骨架屏
骨架屏通常用于在页面完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。
建议开发者在页面数据未准备好时(例如需要通过网络获取),尽量避免展示空白页面,而是先通过骨架屏展示页面的大致结构,请求数据返回后再进行页面更新。以提升用户的等待意愿。
4.其它优化建议
合理使用 setData
setData
是小程序开发中使用最频繁、也是最容易引发性能问题的接口。1. setData 的流程
setData
的过程,大致可以分成几个阶段:
- 逻辑层虚拟 DOM 树的遍历和更新,触发组件生命周期和 observer 等;
- 将 data 从逻辑层传输到视图层;
- 视图层虚拟 DOM 树的更新、真实 DOM 元素的更新并触发页面渲染更新。
2. 数据通信
对于第 2 步,由于小程序的逻辑层和视图层是两个独立的运行环境、分属不同的线程或进程,不能直接进行数据共享,需要进行数据的序列化、跨线程/进程的数据传输、数据的反序列化,因此数据传输过程是异步的、非实时的。
iOS/iPadOS/MacOS 上,数据传输是通过
evaluateJavascript
实现的,还会有额外 JS 脚本解析和执行的耗时。数据传输的耗时与数据量的大小正相关,如果对端线程处于繁忙状态,数据会在消息队列中等待。
3. 使用建议
3.1 data 应只包括渲染相关的数据
setData 应只用来进行渲染相关的数据更新。用 setData 的方式更新渲染无关的字段,会触发额外的渲染流程,或者增加传输的数据量,影响渲染耗时。
- ✅ 页面或组件的 data 字段,应用来存放和页面或组件渲染相关的数据(即直接在 wxml 中出现的字段);
- ✅ 页面或组件渲染间接相关的数据可以设置为「纯数据字段」,可以使用 setData 设置并使用 observers 监听变化;
- ✅ 页面或组件渲染无关的数据,应挂在非 data 的字段下,如
this.userData = {userId: 'xxx'}
;- ❌ 避免在 data 中包含渲染无关的业务数据;
- ❌ 避免使用 data 在页面或组件方法间进行数据共享;
- ❌ 避免滥用 纯数据字段 来保存可以使用非 data 字段保存的数据。
3.2 控制 setData 的频率
每次 setData 都会触发逻辑层虚拟 DOM 树的遍历和更新,也可能会导致触发一次完整的页面渲染流程。过于频繁(毫秒级)的调用
setData
,会导致以下后果:
- 逻辑层 JS 线程持续繁忙,无法正常响应用户操作的事件,也无法正常完成页面切换;
- 视图层 JS 线程持续处于忙碌状态,逻辑层 -> 视图层通信耗时上升,视图层收到消息的延时较高,渲染出现明显延迟;
- 视图层无法及时响应用户操作,用户滑动页面时感到明显卡顿,操作反馈延迟,用户操作事件无法及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。
因此,开发者在调用 setData 时要注意:
- ✅ 仅在需要进行页面内容更新时调用 setData;
- ✅ 对连续的 setData 调用尽可能的进行合并;
- ❌ 避免不必要的 setData;
- ❌ 避免以过高的频率持续调用 setData,例如毫秒级的倒计时;
- ❌ 避免在 onPageScroll 回调中每次都调用 setData。
3.3 选择合适的 setData 范围
组件的 setData 只会引起当前组件和子组件的更新,可以降低虚拟 DOM 更新时的计算开销。
- ✅ 对于需要频繁更新的页面元素(例如:秒杀倒计时),可以封装为独立的组件,在组件内进行 setData 操作。必要时可以使用 CSS contain 属性限制计算布局、样式和绘制等的范围。
3.4 setData 应只传发生变化的数据
setData 的数据量会影响数据拷贝和数据通讯的耗时,增加页面更新的开销,造成页面更新延迟。
- ✅ setData 应只传入发生变化的字段;
- ✅ 建议以数据路径形式改变数组中的某一项或对象的某个属性,如
this.setData({'array[2].message': 'newVal', 'a.b.c.d': 'newVal'})
,而不是每次都更新整个对象或数组;- ❌ 不要在 setData 中偷懒一次性传所有data:
this.setData(this.data)
。3.5 控制后台态页面的 setData
由于小程序逻辑层是单线程运行的,后台态页面去
setData
也会抢占前台页面的运行资源,且后台态页面的的渲染用户是无法感知的,会产生浪费。在某些平台上,小程序渲染层各 WebView 也是共享同一个线程,后台页面的渲染和逻辑执行也会导致前台页面的卡顿。
- ✅ 页面切后台后的更新操作,应尽量避免,或延迟到页面
onShow
后延迟进行;- ❌ 避免在切后台后仍进行高频的 setData,例如倒计时更新。
4. 性能分析
开发者可以通过组件的 setUpdatePerformanceListener 接口获取更新性能统计信息,来分析产生性能瓶颈的组件。
28.什么是虚拟DOM?
它是什么?
一个能代表真实DOM树的对象,通常含有标签名、标签上的属性、事件监听和子元素,以及其它属性。
它是和真实DOM对比的一个概念。
当需要操纵时,可以在虚拟DOM的内存中执行计算和操作,而不是在真实DOM上进行操纵。这自然会更快,并且允许虚拟DOM算法计算出最优化的方式来更新实际DOM结构,一旦计算出,就将其应用于实际的DOM树,这就提高了性能。
虚拟DOM的优点:
29.什么是diff算法?
我们修改了文本内容后会生成新的虚拟dom,新旧俩个虚拟dom之间是存在一定差异的,如果能快速找到这俩个对象之间的差异,就可以最小化的更新视图,"diff算法"就是专门用来比对这两个虚拟dom对象的。
diff算法的目的:找出差异,最小化的更新视图。