一. JS基础
0. 从 2 个看似 基本 的问题开始
1) 问题1: 变量相关问题
变量到底是什么?
- 可以变化的量, 由变量名和变量值组成, 名称是标识, 值是可以变化的
- 一个变量对应一块小内存, 变量名是这块内存的标识名称, 用于查找这块内存, 变量的值就是内存中保存的数据
基本类型变量与引用类型变量的区别是什么?
根据变量内存中保存的数据来区分
基本类型变量: 内存中保存的基本类型的数据
引用类型变量: 内存中保存的是地址值
函数传参, 到底是值传递还是引用传递?
函数调用时, 是将实参变量的数据拷贝一份赋值给形参变量
只是实参变量数据可能是基本类型, 也可能是引用类型的(地址值)
给变量赋值到底做了什么?
将基本类型数据赋给变量: 将数据保存在变量内存中
将对象赋给变量: 将对象在内存中的地址值保存在变量内存中
将另一个变量赋给变量: 将右侧变量内存中的数据(基本数据/地址值数据)拷贝保存到左侧变量内存
有哪些改变变量值的方式?
只能通过赋值改变
c.m = 2: 改的是c变量指向的对象内部的属性数据, c本身没有变化的(对象内存的位置没变)
如何理解下面 2 句重要的话(编码演示说明)?
- 2个引用变量指向同一个对象, 如果通过一个变量修改对象内部的数据 => 另一个变量也能看到原对象 (新数据)
- 2个引用变量指向同一个对象, 让其中一个变量指向一个新的对象 => 另一个引用变量看到的是老的对象
2) 问题2: 表达式a.b的内部解析流程
查找a: 查找变量, 沿着作用域链查找
找不到 => 报错(ReferenceError/引用错误 ): a is not defined
找到了: 得到a的值, 准备去.b, 但a的值不同, 处理结果不同
undefined/null ==> 报错(TypeError/类型错误):can not read property b of undefined/null
boolean/number/string基本值 ==> 创建一个包含这个值的对应的包装类型对象
引用地址值 ==> 找到对应的对象 ==> 准备找b
查找b属性: 先在对象自身上找, 如果没找到, 沿着原型链查找
找不到: 返回undefined
找到了, 返回它的值(值的拷贝)
1. 作用域与闭包相关
1) 作用域
变量可以合法使用(查找)的范围
2) 作用域分类
- 全局作用域
- 函数作用域
- 块级作用域(ES6新增): const / let
- 问题: 什么是块作用域?
3) 作用域链
多个嵌套的作用域形成的由内向外的结构, 用于查找变量(本质是包含由内向外的多个变量对象的数组)
4) 变量提升与函数提升
- 变量提升(变量声明提升): 在变量定义语句之前, 就可以访问到这个变量(undefined)
- 函数提升(函数声明提升): 在函数定义语句之前, 就执行该函数
- 原因: 简单来说就是在执行全局代码和函数前会进行预解析/处理
- 将var变量声明放在最前面执行
- 将function函数声明放在最前面执行
- 函数提升 ==> 变量提升, 同名的变量忽略
1. 执行函数定义: 创建函数对象, 如果函数有名称, 还会定义函数名称变量, 并将函数对象赋值给这个变量
2. 执行函数: 执行函数对象内部包含语句(函数对象必须已经存在了)
- 区分执行函数定义与执行函数
- 执行函数定义: 创建函数对象, 如果指定了函数, 同时会定义变量并指向这个函数对
- 执行函数: 执行函数内部的语句
- 必须先执行函数定义, 再执行函数 ===> 注意: 函数定义有可能会提升到最上面执行
5) 闭包
理解:
- 当嵌套的内部函数引用了外部函数的变量时就产生了闭包
- 什么时候产生的闭包? 执行内部函数定义(创建函数对象)后
通过chrome工具得知: 闭包本质是内部函数中的一个对象(非js的容器), 这个容器中包含引用的变量
作用
- 延长局部变量的生命周期
- 让函数外部能操作内部的局部变量
写一个闭包程序
function fn1() {
var a = 2 ;
var b = 3
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1();
f();
f();
f = null
f = 1
function showDedelay (time, msg) {
setTimeout(() => {
alert(msg)
}, time)
}
showDelay( 1000 )
区别闭包的三个操作
- 产生闭包: 内部函数对象创建时产生, 包含那个被引用的变量的容器(不是js对象)
- 使用闭包: 执行内部函数
- 释放闭包: 让内部函数对象成为垃圾对象, 断开指向它的所有引用
闭包的应用:
删除列表中的的某个商品的例子(带确定框)
1
6) this在不同场景下的取值?
this学习的 2 个目标:
- 能判断出函数中的this是谁
- 能控制函数的this是需要的特定对象
常规情况下, 函数中的this取决于执行函数的方式
- fn(): 直接调用 ==> this是? window
- new fn(): new调用 ==> this是? 新建的对象
- obj.fn(): 通过对象调用 ==> this是? obj
- fn.call/apply(obj): 通过函数对象的call/apply来调用 ==> this是? obj
特殊情况:
- bind(obj)返回的函数 ==> this是? obj
- 箭头函数 ==> this是? 外部作用域的this
- 回调函数 它不是我们调用的
- 定时器/ajax/promise/数组遍历相关方法回调 ==> this是? window
- vue控制的回调函数 ==> this是? 组件的实例
- React控制的生命周期回调, 事件监听回调 ==> this是? 组件对象 / undefined
如何控制函数的this?
- 利用函数的bind()
- 利用箭头函数
- 也可以用外部保存了this的变量
2. 原型相关
1) 原型
每个函数都有一个显示原型属性:
prototype
每个实例都有一个隐式原型:
__proto__
实例的
__proto__
与对应函数的prototype
都指向原型对象原型对象上有一个
constructor
属性指向对应的构造函数function Fn () {}
const fn = new Fn()
问题1: 区别 执行函数定义 与 执行函数
- 执行函数定义: 也就是去创建函数对象, 只是有可能被JS引擎提升预处理执行
- 执行函数: 执行函数体中所有语句
问题2: 说说函数对象上的 prototype属性?
执行函数定义(有可能被提升执行)创建函数对象
给函数对象添加prototype属性, 属性值为空的Object实例对象, 也就是原型对象
给原型对象添加constructor属性, 值为函数
伪代码:
jsnew Function() this.prototype = {} // this就是函数对象 this.prototype.constructor = this
问题3: 说说实例对象上的
__proto__
属性?JS引擎在创建实例对象时内部自动执行时, 会自动给实例对象添加__proto__属性, 值为构造函数的 prototype属性的值
伪代码:
jsthis.__proto__ = Fn.prototype // this是实例对象
2) 原型链
从对象的
__proto__
开始, 连接的所有对象, 就是我们常说的原型链, 也可称为隐式原型链查找对象属性简单说: 先在自身上查找, 找不到就沿着原型链查找,如果还找不到返回
undefined
function Foo () {} const f = new Foo()
3) 查找对象上属性的基本流程
- 先在对象自身上查找, 如果有, 直接返回
- 如果没有, 根据·在原型对象上查找, 如果有, 直接返回
- 如果没有根据原型对象的
__proto__
在原型对象的原型对象上查找, 一直查找到Object原型对象为止如果找到了返回, 如果查找不到由于它的__proto__
为null, 只能返回undefined - 如果找到了返回, 如果查找不到由于它的
__proto__
为null, 只能返回undefined
4) instanceof
- 作用: 判断一个任意类型对象的具体类型
- 如何判断?
- 对于 A instanceof B
- A是实例对象, B是构造函数
- 如果B的prototype属性所指向的原型对象是A实例对象的原型链接上的某个对象, 返回true, 否则返回false
5) 原型与原型链结构图
function Foo () {}
const f1 = new Foo()
const f2 = new Foo()
const o1 = new Object()
const o2 = {}
Foo instanceof Object true
Foo instanceOf Function true
Object instanceof Object true
Function instanceof Function true
Function instanceof Object true
Object instanceof Foo
f1 instanceof Function
f1 instanceof Object

6) 继承
使用构造函数和ES6的类设计并测试以下需要
需求1:
现在 2 个人, 都有姓名与年龄(值自己指定), 都能自我介绍自己的姓名和年龄, 能得到当 前的总人数
需求2:
现在又有了一个学生, 它有身价的属性, 能介绍它的所有信息
又有一个老师, 它有教授课程的属性, 能介绍它的所有信息
- 方式一 : 基于构造函数的继承 : 原型链 + 借用构造函数的组合式继承
- 借用父类型构造函数 : Person.call(this, name, age)
- 让子类的原型为父类的实例 : Student.prototype = new Person()
- 让子类型原型的构造器为子类型 : Student.prototype.constructor = Student
// 父类型
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.fn = function () {}
Person.prototype.sayHello = function () {
console.log(`我叫${this.name}, 年方${this.age}`)
}
// 子类型
function Student(name, age, price) {
// this.name = name
// this.age = age
// 借用父类型的构造函数
Person.call(this, name, age) // 相当于执行this.Person(name, age)
this.price = price
}
// 让子类的原型为父类的实例
Student.prototype = new Person()
// 让原型对象的构造器为子类型
Student.prototype.constructor = Student
Student.prototype.sayHello = function () {
console.log(`我叫${this.name}, 年方${this.age}, 身价: ${this.price}`)
}
const s = new Student('tom', 23 , 14000 )
s.sayHello()
s.fn()
方式二 : 基于 class/ 类的继承
子类 extends 父类 : class Teacher extends Person
子类构造器中调用父类的构造 : super(name, age)
js//父类 class Person2 { constructor (name, age) { this.name = name this.age = age } fn () {} sayHello () { console.log(`我叫${this.name}, 年方${this.age}`) } } // 子类 class Teacher extends Person2 { constructor (name, age, course) { super(name, age) this.course = course } // 重写父类的方法 sayHello () { console.log(`我叫${this.name}, 年方${this.age}, 课程:${this.course}`) } } const t = new Teacher('bb', 34 , 'CC') t.sayHello() t.fn()
7) 面向对象的 3 大特性
封装:
- 将可复用的代码用一个结构包装起来, 后面可以反复使用
- js的哪些语法体现了封装性: 函数 ==> 对象 ==> 模块 ==> 组件 ==> 库
- 封装都要有个特点: 不需要外部看到的必须隐藏起来, 只向外部暴露想让外部使用的功能或数据
继承
- 为什么要有继承? 复用代码, 从而减少编码
- js中的继承都是基于原型的继承: ES6的类本质也是
- 编码实现: 原型链+借用构造函数的组合
多态
理解
- 声明时指定一个类型对象, 并调用其方法
- 实际使用时可以指定任意子类型对象, 运行的方法就是当前子类型对象的方法
JS中有多态: 由于JS是弱类型语言, 在声明时都不用指定类型 在使用时可以指定任意类型的数据 ==> 这已经就是多态的体现了
javaJava test (Person p) {p.sayInfo()} test(new Person()) test(new Teacher()) // Teacher类型中定义的sayInfo() test(new Student()) // Student类型中定义的sayInfo()
jsJS中 function test (p) {p.sayInfo()} test(new Teacher()) test(new Student())
3. 异步相关
单线程与异步
JS是单线程编程语言, 只能同时做一件事
js引擎是在一个线程(可以称为JS线程)上解析执行js代码的, 无论是同步代码还是异步代码
浏览器(H5)和 nodejs 已支持通过Web Worker语法让 JS 启动分线程, 但很少使用
JS线程与GUI线程
- 2 个不同的线程
- JS线程负责执行JS代码, GUI线程负责渲染页面
- 但 2 个线程是互斥的, 不会同时执行 (了解原因: JS 可以修改 DOM 结构)
界面第一次渲染:
- 第一套: script宏任务 ==> 所有的微任务==> 渲染界面
- 第二套: 取出宏队列的第一个宏任务执行==> 所有的微任务==> 渲染界面
- 反复按第二套流程执行, 直到宏队列与微队列为空的
界面更新渲染:
- 执行第一个宏任务 => 所有的微任务==> 渲染界面
JS遇到需要等待 (网络请求, 定时任务) 不能卡住
需要异步 ==> 回调函数

<ul>
<li>aaa</li>
<li>bbb</li>
<li>ccc</li>
</ul>
<button id="test">test</button>
<div id="content">
aaaaaaa
</div>
<script>
Promise.resolve().then(() => { // 微任务
alert('promise1')
})
Promise.resolve().then(() => { // 微任务
alert('promise2')
})
setTimeout(() => {// 宏任务
alert(document.getElementById('content').innerHTML)
}, 0 )
document.getElementById('test').onclick = () => {// 宏任务
document.getElementById('content').innerHTML = 'xxxx' // dom渲染
Promise.resolve().then(() => { // 微任务
alert('promise3')
})
setTimeout(() => {// 宏任务
alert(document.getElementById('content').innerHTML)
}, 0 );
}
</script>
** 同步与异步**
基于 JS 是单线程编程语言
同步会阻塞代码执行
异步不会阻塞代码执行: ==> ajax请求, send()发异步/同步请求的对比
使用Promise解决回调地狱问题
Promise相对纯回调形式, 指定回调函数的时机更灵活(可以在发送请求后或请求结束后)
event loop
JS是通过事件循环机制来实现JS的单线程异步
js是单线程运行的
异步要基于回调来实现
event loop 就是异步回调的实现原理
事件循环机制的 2 个重要部分
在分线程执行的管理模块: 定时器/ajax/dom事件
保存待执行的回调函数的事件队列(Event queue)/任务队列(Task queue)
宏任务与微任务
宏任务:
script(整体代码)
setTimeout,
setInterval,
Ajax,
DOM事件监听
postMessage (H5, 向其它窗口分发异步消息)
setImmediate(立即执行, Node.js 环境)
微任务:
Promise,
async/await,
mutationobserver(H5, 监视DOM元素变化)
宏队列与微队列
宏队列: 用来保存n个宏任务的队列容器
微队列: 用来保存n个微任务的队列容器
event loop
js-web中的dom事件回调, 定时器回调与ajax回调异步执行, 都是基于event loop ===> 宏任务
H5新的MutationObserver也基于event loop ==> 微任务
promise成功和失败的回调也是异步执行的, 也是基于event loop ==> 微任务
执行顺序:
第一步: 先执行script宏任务
第二步: 再依次取出微列中的所有微任务执行 ==> UI线程更新界面
第三步: 再取出宏队列中第一个宏任务执行
第四步: 再依次取出微列中的所有微任务执行 ==> UI线程更新界面
后面重复三,四步
Promise的理解
ES6推出的新的更好的异步编程解决方案(相对于纯回调的方式)
可以异步操作启动后或完成后, 再指定回调函数得到异步结果数据
解决嵌套回调的回调地狱问题 ---promise then的链式调用
promise对象有 3 种状态
pending
resolved/fulfilled
rejected
promise状态的 2 种变化
pending --> resolved
pending --> rejected
注意: 变化是不可逆, 且只能变一次
promise的then()的理解
then()总是返回一个新的promise
新promise的结果状态由then指定的回调函数执行的结果决定
抛出错误
返回失败的promise
返回成功的promise
返回其它任何值
Promise.all([promise1, promise2, promise3])
问题:
如何在发送的多个请求成功后再显示数据?
如何发 3 请求成功后再发第 4 个请求?
Promise.all([p1, p2, p3]).then(values => {
return axios.get(url4)
}).then(value4 => {
// 就可以处理了
})
批量/一次性发送多个异步请求
当都成功时, 返回的promise才成功
一旦有一个失败的, 返回的promise就失败了
async/await与promise的关系
async/await是消灭异步回调的终极武器
作用: 简化promise对象的使用, 不用再使用then/catch来指定回调函数
它和Promise并不互斥, 两者相辅相成
执行async函数, 返回promise对象
await相当于promise的then
try...catch可捕获异常, 相当于promise的catch
几个面试题
测试题 1
- 测试题
- 测试题
测试题 4
测试题 5
setTimeout(() => {
console.log("0")
}, 0 )
new Promise((resolve,reject)=>{
console.log("1")
resolve()
}).then(()=>{
console.log("2")
new Promise((resolve,reject)=>{
console.log("3")
resolve()
}).then(()=>{
console.log("4")
}).then(()=>{
console.log("5")
})
}).then(()=>{
console.log("6")
})
new Promise((resolve,reject)=>{
console.log("7")
resolve()
}).then(()=>{
console.log("8")
})
T
W
H
1 7 2 3 8 4 6 5 0
4. 内存管理
MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management
内存的生命周期
垃圾回收机制
内存溢出
1. 分配你所需要的内存
- 定义变量
- 创建对象
- 调用函数
2. 使用分配到的内存(读、写)
- 读取当前值
- 设置新的值
3. 不需要时将其释放\归还
- 函数执行完, 为当次函数执行分配的内存自动释放
- 没有引用指向的对象, 会在某个时刻被垃圾回收器(单独线程)回收释放
机制 1 :引用计数法
最初级的垃圾收集算法,现在都不用了,把“对象是否不再需要”简化定义为“对象有没有引用指向它“
每个对象内部都标记一下引用它的总个数, 如果引用个数为 0 时,即为垃圾对象
问题: 循环引用有问题
var o1= {};
var o2 = {};
o1.a = o2; // o 引用 o2 引用计数为 2
o2.a = o1 // o2 引用 o 引用计数为 2
o1 = null 引用计数为 1
o2 = null 引用计数为 1
// 2个对象都有引用指向它, 所以都不是垃圾对象
机制 2 :标记-清除法
当前垃圾回收算法的基础, 它“对象是否不再需要”简化定义为“对象是否可以获得”
从根对象(也就是window)开始查找所有引用的对象, 并标记为‘使用中’,没有标记为使用中的对象就
是垃圾对象
循环引用的对象也可能是垃圾对象
var o1= {};
var o2 = {};
o1.a = o2; // o 引用 o
o2.a = o; // o2 引用 o
o1 = null
o2 = null
// 通过window找不到这 2 个对象, 它们就没有被标记为‘使用中‘,就当作垃圾对象回收释放
运行程序需要分配的内存超过了系统能给你分配的最大剩余内存 ==》 抛出内存溢出的错误,程序中断运行
const arr = []
for (let index = 0; index < 100000000; index++) {
arr[index] = new Array(1000)
}
内存泄露
二. ES6+
1. ES6+整体新语法列表
说明 : 跟面试官交流 ES6, 先快速说出这套列表 , 再选择几个常用的 / 有些难度的 / 有说头的说 , 或者看面试 官关注哪方面的
1. const与let
2. 解构赋值
1. 字符串的扩展
2. 数值的扩展
3. 函数的扩展
4. 数组的扩展
5. 对象的扩展
6. 类语法
7. 模块化语法
当程序中的某个内存数据不再需要使用, 而由于某种原因, 没有被释放 ==》 称之为“内存泄露”了
导致内存泄露的原因有下面几种常见情况:
1. 意外的全局变量
function fn (
a = new Array(100000)
)
fn()
- 没有及时清除的定时器 this.intervalId = setInterval(() => {}, 1000) // clearInterval(this.intervalId)
- 没有及时解绑的监听 this.$bus.$on('xxx', this.handle) // this.$bus.$off('xxx')
- 没有及时释放的闭包
8. 异步语法
9. 新容器语法
10. 代理(Proxy)与反射(Reflect)语法
2. 面试可说的 ES6 常用语法列表
const与let
箭头函数
解构赋值
形参默认值
rest/剩余参数
类语法: class / extends / constructor / static /super
扩展运算符: ...
模板字符串
异步语法: promise / async & await
对象的属性与方法简写
set / map
模块化语法: export / default / import / import()
3. ES6常用语法详细列表
const与let
const 定义常量 , let 定义变量
相对于 var
有块作用域
没有变量提升
不会添加到 window 上
不能重复声明
解构赋值
解构对象: const {id, name} = this.product
解构数组: const [count, setCount] = useState()
形参解构: add ({ id, title }) {}
引入模块解构: import { getProductList } from '@/api'
字符串的扩展
模板字符串 : 我是${name}, 今年${age}
方法: includes() / startsWith() / endswith()
数值的扩展
完善二进制(0b)与八进制(0o)表示
给Math添加方法: parseInt()与parseFloat() (原本window上有)
指数计算: **
函数的扩展
箭头函数
没有自己的 this, 使用外部作用域中的 this, 不能通过 bind 来绑定 this
不能通过 new 来创建实例对象
内部没有 arguments, 可以通过 rest 参数来代替
形参默认值: fn (a=2, b={}) {}
rest参数: fn (a, ...args) {} / fn (...args) {} fn(1, 2, 3, 4)
数组的扩展
扩展运算符
浅拷贝数组 : const arr2 = [...arr]
合并多个数组: const arr3 = [...arr1, ...arr2]
静态方法
Array.from(): 将类数组对象和可遍历对象转为真数组
Array.from(new Set(arr))
[...new Set(arr)]
Array.of(1, 2, 3): 将一组值,转换为数组
实例方法
find() / findIndex(): 查找匹配的元素或下标
arr.flat(): 将多维数组转为一维数组(也称为: 数组扁平化) [1, [2, 3]] ==> [1, 2, 3]
对象的扩展
扩展运算符
浅拷贝对象 : const obj2 = {...obj1}
合并多个对象: const obj3 = {...obj1, ...obj2}
属性/方法的简洁写法: {name, getName () {}}
遍历内部属性
for..of: 遍历对象及其原型链上所有属性
Object.keys(obj): 得到对象自身可遍历的所有属性名的数组
静态方法:
Object.is(value1, value2): 判断 2 个值是否完全一样 NaN===NaN
Object.assign(target, ...sources): 将后面任意多个对象合并到target对象上
类语法
class
extends
constructor
super() / super.xxx()
static
模块化语法
export
export default value
import: 静态导入, 合并一起打包
import() : 动态导入 , 拆分打包 , 用于懒加载 const Home = () => import('./views/Home.vue')
import('./views/Home.vue').then((module) => {
// 使用module块
module.default
module.xxx
})
异步语法
Promise
async 函数
await 表达式
新容器语法
Map key1:value1, key2: value2 跟对象的最大区别大于key可以是任意类型, 对象的key只是
string
Set value1, value2, value3 value不会重复, 如果有自动过滤掉
代理(Proxy)与反射(Reflect)语法
Proxy
Reflect
三. Web API
1. 事件冒泡与事件委托
1) 事件冒泡的流程
基于DOM树形结构
事件在目标元素上处理后, 会由内向外(上)逐层传递
应用场景: 事件代理/委托/委派
2) 事件委托
减少内存占用(事件监听回调从n变为1) event.target
动态添加的内部元素也能响应
不要滥用
3) 封装一个绑带事件监听的函数(有难度)
2. 前后台交互 & ajax
1) 区别ajax请求与一般HTTP请求
ajax请求是一种特别的http请求
对服务器端来说, 没有任何区别, 区别在浏览器端
浏览器端发请求: 只有XHR或fetch发出的才是ajax请求, 其它所有的都是非ajax请求
浏览器端接收到响应
一般请求: 浏览器一般会直接显示响应体数据, 也就是我们常说的刷新/跳转页面
ajax请求: 浏览器(ajax引擎)不会对界面进行任何更新操作, 只是调用监视的回调函数并传入响
应相关数据
/*
绑定事件监听的通用函数(不带委托)
*/
function bindEvent1 (ele, type, fn) {
ele.addEventListener(type, fn)
}
/*
绑定事件监听的通用函数(带委托)
*/
function bindEvent2(ele, type, fn, selector) {
ele.addEventListener(type, event => {
// 得到发生事件的目标
const target = event.target
if (selector) {
// 如果元素被指定的选择器字符串选择, 返回true; 否则返回false。
if (target.matches(selector)) {
// 委托绑定调用
fn.call(target, event)
}
} else {
// 普通绑定调用
fn.call(ele, event)
// fn(event) // this不对
}
})
}
<ul>
<span>
<li>
<li>
</ul>
bindEvent2(ul, 'click', (event) => {}, 'li')
bindEvent2(ul, 'click', (event) => {})
2) xhr.status
2XX: 表示成功处理请求, 如200, 201
3XX: 需要重定向, 浏览器直接跳转, 如 302
4XX: 客户端请求错误, 如: 401, 404
5XX: 服务器端错误, 如: 500
3) 封装一个简易的ajax异步请求函数
/*
xhr + promise 封装一个异步ajax请求的通用函数 简洁版
*/
function ajax(url) {
return new Promise((resolve, reject) => {
// 创建一个XHR对象
const xhr = new XMLHttpRequest()
// 初始化一个异步请求(还没发请求)
xhr.open('GET', url, true)
xhr.onreadystatechange = function () {
/*
ajax引擎得到响应数据后
将xhr的readyState属性指定为 4
将响应数据保存在response / responseText属性上
调用此回调函数
*/
// 如果状态值不为4, 直接结束(请求还没有结束)
if (xhr.readyState !== 4 ) {
return
}
// 如果响应码在200~~299之间, 说明请求都是成功的
if (xhr.status>= 200 && xhr.status< 300 ) {
// 指定promise成功及结果值
resolve(JSON.parse(xhr.responseText))
} else { // 请求失败了
// 指定promise失败及结果值
reject(new Error('request error staus '+ request.status))
}
}
xhr.send(null)
})
}
/*
xhr + promise 封装一个异步ajax请求的通用函数 加强版
返回值: promise
参数为配置对象
url: 请求地址
params: 包含所有query请求参数的对象
data: 包含所有请求体参数数据的对象
method: 为请求方式
*/
function axios({url, params={}, data={}, method='GET'}) {
// 返回一个promise对象
return new Promise((resolve, reject) => {
// 创建一个XHR对象
const request = new XMLHttpRequest()
// 根据params拼接query参数 let queryStr = Object.keys(params).reduce((pre, key) => { pre += &${key}=${params[key]}
return pre }, '') if (queryStr.length> 0 ) { queryStr = queryStr.substring( 1 ) url += '?' + queryStr } // 请求方式转换为大写 method = method.toUpperCase()
// 初始化一个异步请求(还没发请求) request.open(method, url, true) // 绑定请求状态改变的监听 request.onreadystatechange = function () { // 如果状态值不为4, 直接结束(请求还没有结束) if (request.readyState !== 4 ) { return } // 如果响应码在200~~299之间, 说明请求都是成功的 if (request.status>= 200 && request.status< 300 ) { // 准备响应数据对象 const responseData = { data: JSON.parse(request.response), status: request.status, statusText: request.statusText } // 指定promise成功及结果值 resolve(responseData) } else { // 请求失败了 // 指定promise失败及结果值 const error = new Error('request error staus '+ request.status) reject(error) } }
// 如果是post/put请求 if (method==='POST' || method==='PUT' || method==='DELETE') { // 设置请求头: 使请求体参数以json形式传递 request.setRequestHeader('Content-Type', 'application/json;charset=utf-8') // 包含所有请求参数的对象转换为json格式 const dataJson = JSON.stringify(data) // 发送请求, 指定请求体数据 request.send(dataJson) } else {// GET请求 // 发送请求 request.send(null) } }) }
4) 跨域
什么是跨域(同源策略)
JSONP
CORS
代理服务器
5) 同源策略
同源: 协议, 域名, 端口, 三者都相同
ajax请求时, 浏览器要求当前网页和Server必须同源(安全), 否则会抛出跨域的错误
加载image/link/script不受同源策略限制
6) JSONP原理
前台:
接收响应数据的函数: function fn (data) {}
后台
处理请求, 产生需要返回的数据data
读取callback请求参数得到前台处理响应数据的函数名fn
返回执行函数fn的js代码: 'fn && fn(data)'
不足
只能处理GET请求
每个请求在后台都要做处理, 麻烦
// 发送jsonp请求的函数
function jsonp() {
var script = document.createElement('script')
script.type = 'text/javascript'
// 传参并指定回调执行函数为backFn
script.src = 'http://localhost:4000/getUserInfo?id=100&callback=fn'
document.body.appendChild(script)
}
// 回调函数, 接收响应数据
function fn(data) {
console.log(data)
}
后台:
接收到请求后, 处理请求, 生成要返回的结果数据data
// response.body = data
const fn = request.query.callback
response.body = `${fn}(${data})`
7) CORS原理
后台: 返回允许浏览器在某个域上发送跨域请求的相关响应头
前台: 不需要做特别任何处理
8) 代理服务器
开发环境: 利用webpack-dev-server中的http-proxy-middleware 进行正向代理
vue脚手架项目
react脚手架项目
自定义webpack配置
直接使用http-proxy-middle配置
生产环境: 利用nigix 进行反向代理
https://www.cnblogs.com/taostaryu/p/10547132.html
9) 使用axios发送ajax请求
// 使用cors, 允许跨域, 且允许携带跨域cookie
app.use(function (req, res, next) {
// console.log('----')
// 允许跨域的地址
res.header('Access-Control-Allow-Origin', 'http://localhost:5500') // 不要是*
// 允许携带凭证(也就是cookie)
res.header('Access-Control-Allow-Credentials', 'true')
// 允许跨域的请求头
res.set("Access-Control-Allow-Headers", "Content-Type")
// 放行
next()
})
// axios封装的写法
axios.defaults.withCredentials = true // 允许携带cookie
// 原生写法
xhr.withCredentials = true
axios(url)
axios({
method: '',
url: '', // 如果有params参数必须拼接在url中
params: {}, // query参数 如何携带params参数? 只能拼接在url上
data: {} // 请求体参数
})
axios.get(url, {配置}) get(url, {params: {username: 'xxx'}})
axios.post(url, data, {配置})
axios.put(url, data, {配置})
axios.delete(url, {配置})
// 返回的是一个函数 ==> 函数的功能与axios类似
// service与axios的配置不太一样, 而且可以根据需要产生多个
const service = axios.create({
baseURL: '/api',
timeout: 20000 , })
service.interceptors.request.use((config) => { // 添加请求头 config.headers['token'] = token值 return config // 必须返回config })
// 根据config使用xhr发送ajax请求 new Promise((resolve, reject) => { xhr.send() 成功了, resolve(respnose) 失败了, reject(error) })
service.interceptors.response.use( response => { // return response return response.data }, error => { throw error } )
service({}).then(data => {
}).catch(error => {
})
// 利用Promise的then的链式调用来串联这些任务的 请求拦截器的回调函数 xhr发请求 响应拦截器成功/失败的回调 具体请求成功/失败的回调
Promise.resolve(config) .then(config => { //... return config }) .then(config => { return new Promise((resolve, reject) => { // 根据config使用xhr发送ajax请求 xhr.send() 成功了, resolve(respnose) 失败了, reject(error) }) }) .then( response => { // return response return response.data },
10) axios二次封装
1. 配置通用的基础路径和超时
2. 显示请求进度条
1. 显示进度条: 请求拦截器回调
2. 结束进度条: 响应拦截器回调
3. 成功返回的数据不再是response, 而直接是响应体数据response.data
4. 统一处理请求错误, 具体请求也可以选择处理或不处理
5. 每个请求自动携带userTempId的请求头: 在请求拦截器中实现
6. 如果当前有token, 自动携带token的请求头
7. 对token过期的错误进行处理
error => {
throw error
}
)
.then(
data => {},
error => {}
)
Promise.resolve(config)
.then((config) => { // 请求拦截器成功回调
// 添加请求头
config.headers['token'] = token值
// return config // 必须返回config
})
.then((config) => { // 用来发ajax的回调
return new Promise((resolve, reject) => {
xhr.send()
// 根据config使用xhr发请求, 封闭响应相关数据response
resolve(response)
// 请求失败
reject(error)
})
})
.then( // 响应拦截器
(response) => {
return response.data
},
(error) => {
}
)
.then((data) => { // 最终发具体请求的成功回调
})
import axios from 'axios'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import store from '@/store'
import router from '@/router'
NProgress.configure({ showSpinner: false }) // 隐藏右侧的旋转进度条
// 创建instance const instance = axios.create({ // baseURL: 'http://182.92.128.115/api', baseURL: '/api', timeout: 20000 })
// 指定请求拦截器 instance.interceptors.request.use(config => { // 显示进度条: 请求拦截器回调 NProgress.start()
/* 每个请求自动携带userTempId的请求头: 在请求拦截器中实现 */ const userTempId = store.state.user.userTempId config.headers.userTempId = userTempId
/* 6. 如果当前有token, 自动携带token的请求头 */ const token = store.state.user.token if (token) { config.headers.token = token }
return config // 必须返回config })
// 指定响应拦截器
instance.interceptors.response.use( response => { // 成功的回调 // 结束进度条: 响应拦截器回调 NProgress.done() // 成功返回的数据不再是response, 而直接是响应体数据response.data return response.data },
async error => { // 失败的回调 // 结束进度条: 响应拦截器回调 NProgress.done()
// 统一处理请求错误, 具体请求也可以选择处理或不处理 // alert('请求出错: ' + error.message||'未知错误')
// 取出响应对象 const { response } = error // 如果是请求处理出错 if (response && response.status) { // 401说明token非法 if (response.status === 401 ) { // 如果当前没在登陆页 if (router.currentRoute.path!=='/login') { // 分发action去清除用户token信息 await store.dispatch('logout') // 跳转到登陆页面 router.replace('/login') // 提示
11) Restless API 与 Restful API
Restless API
传统的API, 把每个url当作一个功能操作 /deleteUser
同一个url, 后台只进行CRUD的某一种操作
请求方式不决定请求的CRUD操作
一个请求路径只对应一个操作
一般只有GET/POST
Restful API
新式的API, 把每个url当作一个唯一资源 /user/2
同一个url, 可以通过不同类型的请求对后台资源数据进行CRUD四种操作
请求方式来决定了请求在后台进行CRUD的哪种操作
GET: 查询
POST: 添加
PUT: 更新
DELETE: 删除
同一个请求路径可以进行多个操作
请求方式会用到GET/POST/PUT/DELETE
测试: 可以使用json-server快速搭建模拟的rest api 接口
11) 从输入url到渲染出页面的整个过程
1. DNS 解析(查询):将域名地址解析 ip 地址
浏览器 DNS 缓存
计算机 DNS 缓存
路由器 DNS 缓存
网络运营商 DNS 缓存
递归查询
2. TCP 链接:TCP 三次握手 ===> 建立连接
客户端发送服务端:我准备好了,请你准备一下
message.error('登陆已过期, 请重新登陆')
}
} else {
message.error('请求出错: ' + error.message||'未知错误')
}
} else if (!response) { // 网络连接不上服务器
message.error('您的网络发生异常,无法连接服务器')
}
// throw error
return Promise.reject(error) // 将错误向下传递
}
)
// 向外暴露instance
export default instance
服务端发送客户端:我也准备好了,请你确认一下
客户端发送服务端:确认完毕
- 发送请求(请求头/请求行/请求体)
将请求报文发送过去
- 返回响应(响应头/响应行/响应体)
将响应报文发送过来
- 解析渲染页面
遇到 HTML,调用 HTML 解析器,解析成 DOM 树
遇到 CSS,调用 CSS 解析器,解析成 CSSOM 树
遇到 JS,调用 JS 解析器(JS 引擎),解析 JS 代码
可能要修改元素节点,重新调用 HTML 解析器,解析更新DOM 树
可能要修改样式节点,重新调用 CSS 解析器,解析更新 CSSOM 树
将 DOM + CSSOM = Render Tree(渲染树)
layout 布局:计算元素的位置和大小信息
render 渲染:将颜色/文字/图片等渲染上去
- 断开链接:TCP 四次挥手 (断开请求链接 2 次, 断开响应链接 2 次)
客户端发送服务端:请求数据发送完毕,可以断开了
服务端发送客户端:请求数据接受完毕,可以断开了
服务端发送客户端:响应数据发送完毕,可以断开了
客户端发送服务端:响应数据接受完毕,可以断开了
3. 前台数据存储
1) 存储方式
cookie
sessionStorage
localStorage
webSQL / IndexDB:
注意: session后台数据存储
2) cookie
本身用于浏览器和Server通讯: http是无状态态
被 "借用" 到本地存储
可用document.cookie读取或保存
可以利用cookies工具库简化编码
3) cookie的缺点
存储大小有限, 最大4KB
http请求时会自动发送给服务器, 增加了请求的数据量
原生的操作语法不太方便操作cookie
浏览器可以设置禁用
4) localStoarge与sessionStorage
相同点:
纯浏览器端存储, 存储空间在 2.5M 到 10M 之间(各家浏览器不同), 请求时不会自动携带
只能保存文本, 如果是对象或数组, 需要转换为JSON
API相同:
setItem(key, value)
getItem(key, value)
removeitem(key, value)
clear()
浏览器不能禁用
不同点:
localStorage保存在本地文件中, 除非编码或手动删除, 否则一直存在
sessonStorage数据保存在当前会话内存中, 关闭浏览器则清除
5) 区别cookie 与 localStorage和sessionStorage
容量
请求时是否自动携带
API易用性
浏览器是否可禁用
6) 区别cookie与session
cookie保存在浏览器端(前台可以操作)
session保存在服务器端(前台不能操作)
session依赖于cookie(session的id以cookie的形式保存在浏览器端)
7) WebSql 与 IndexDB (了解)
都是浏览器端的数据库, 只有较新的浏览器支持
WebSql: 关系型数据库, 通过sql进行数据的CRUD操作
IndexDB: 非关系型数据库, 类似于Mongodb
进一步学习WebSql: https://www.w3cschool.cn/html5/html5-web-sql.html
进一步学习IndexDB: https://www.ruanyifeng.com/blog/2018/07/indexeddb.html
四. 手写代码
1. 函数的call() / apply() / bind()
2. 函数的节流(throttle)与防抖(debounce)
/*
自定义函数对象的call方法
*/
function call (fn, obj, ...args) {
// 如果传入的是null/undefined, this指定为window
if (obj===null || obj===undefined) {
obj = obj || window
}
// 给obj添加一个方法: 属性名任意, 属性值必须当前调用call的函数对象
obj.tempFn = fn
// 通过obj调用这个方法
const result = obj.tempFn(...args)
// 删除新添加的方法
delete obj.tempFn
// 返回函数调用的结果
return result
}
/*
自定义函数对象的apply方法
*/
function apply (fn, obj, args) {
// 如果传入的是null/undefined, this指定为window
if (obj===null || obj===undefined) {
obj = obj || window
}
// 给obj添加一个方法: 属性名任意, 属性值必须当前调用call的函数对象
obj.tempFn = fn
// 通过obj调用这个方法
const result = obj.tempFn(...args)
// 删除新添加的方法
delete obj.tempFn
// 返回函数调用的结果
return result
}
/*
自定义函数对象的bind方法
重要技术:
高阶函数
闭包
call()
三点运算符
*/
function bind (fn, obj, ...args) {
if (obj===null || obj===undefined) {
obj = obj || window
}
return function (...args2) {
return call(fn, obj, ...args, ...args2)
}
}
3. 数组去重(unique)
/*
实现函数节流的函数
*/
function throttle(callback, delay) {
let start = 0 // 必须保存第一次点击立即调用
return function (event) { // 事件回调函数
// this是发生事件的dom元素
console.log('throttle 事件')
const current = Date.now()
if (current - start > delay) { // 从第 2 次点击开始, 需要间隔时间超过delay
callback.call(this, event)
// 将当前时间指定为start, ==> 为后面的比较做准备
start = current
}
}
}
/*
实现函数防抖的函数
*/
function debounce(callback, delay) {
return function (event) {
console.log('debounce 事件...')
// 清除待执行的定时器任务
if (callback.timeoutId) {
clearTimeout(callback.timeoutId)
}
// 每隔delay的时间, 启动一个新的延迟定时器, 去准备调用callback
callback.timeoutId = setTimeout(() => {
callback.call(this, event)
// 如果定时器回调执行了, 删除标记
delete callback.timeoutId
}, delay)
}
}
/*
方法1: 利用forEach()和indexOf()
说明: 本质是双重遍历, 效率差些
*/
function unique1 (array) {
const arr = []
array.forEach(item => {
if (arr.indexOf(item)===- 1 ) { // 内部在遍历判断出来的
arr.push(item)
}
})
return arr
}
4. 数组扁平化(flatten)
/*
方法2: 利用forEach() + 对象容器
说明: 只需一重遍历, 效率高些
*/
function unique2 (array) {
const arr = []
const obj = {}
array.forEach(item => {
if (!obj.hasOwnProperty(item)) {// 不用遍历就能判断出是否已经有了
obj[item] = true
arr.push(item)
}
})
return arr
}
/*
方法3: 利用ES6语法
1). from + Set
2). ... + Set
说明: 编码简洁
*/
function unique3 (array) {
// return Array.from(new Set(array))
return [...new Set(array)]
}
/*
数组扁平化: 取出嵌套数组(多维)中的所有元素放到一个新数组(一维)中
如: [1, [3, [2, 4]]] ==> [1, 3, 2, 4]
*/
/*
方法一: 递归 + reduce() + concat()
*/
function flatten1 (array) {
return array.reduce((pre, item) => {
if (Array.isArray(item) && item.some((cItem => Array.isArray(cItem)))) {
return pre.concat(flatten1(item))
} else {
return pre.concat(item)
}
}, [])
}
/*
方法二: ... + some() + concat()
*/
function flatten2 (array) {
let arr = [].concat(...array)
while (arr.some(item => Array.isArray(item))) {
5. 深拷贝
arr = [].concat(...arr)
}
return arr
}
/*
深度克隆
1). 大众乞丐版
问题1: 函数属性会丢失
问题2: 循环引用会出错
2). 面试基础版本
解决问题1: 函数属性还没丢失
3). 面试加强版本
解决问题2: 循环引用正常
4). 面试加强版本2(优化遍历性能)
数组: while | for | forEach() 优于 for-in | keys()&forEach()
对象: for-in 与 keys()&forEach() 差不多
*/
const obj = {
a: {
m: 1
},
b: [ 3 , 4 ],
fn: function (){}
}
obj.a.c = obj.b
obj.b[ 0 ] = obj.a
/*
1). 大众乞丐版
问题1: 函数属性会丢失
问题2: 循环引用会出错
*/
export function deepClone1(target) {
return JSON.parse(JSON.stringify(target))
}
/*
获取数据的类型字符串名
*/
function getType(data) {
return Object.prototype.toString.call(data).slice( 8 , - 1 ) // -1代表最后一位
// [object Array] ===> Array [object Object] ==> Object
}
/*
2). 面试基础版本
解决问题1: 函数属性还没丢失
*/
function deepClone2(target) {
const type = getType(target)
if (type==='Object' || type==='Array') { const cloneTarget = type === 'Array'? [] : {} for (const key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone2(target[key]) } } return cloneTarget } else { return target } }
/* 3). 面试加强版本 解决问题2: 循环引用正常 */ function deepClone3(target, map = new Map()) { const type = getType(target) if (type==='Object' || type==='Array') { // 从map容器取对应的clone对象 let cloneTarget = map.get(target) // 如果有, 直接返回这个clone对象 if (cloneTarget) { return cloneTarget } cloneTarget = type==='Array'? [] : {} // 将clone产生的对象保存到map容器 map.set(target, cloneTarget) for (const key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone3(target[key], map) } } return cloneTarget } else { return target } }
/* 4). 面试加强版本2(优化遍历性能) 数组: while | for | forEach() 优于 for-in | keys()&forEach() 对象: for-in 与 keys()&forEach() 差不多 */ function deepClone4(target, map = new Map()) { const type = getType(target) if (type==='Object' || type==='Array') { let cloneTarget = map.get(target) if (cloneTarget) { return cloneTarget }
if (type==='Array') { cloneTarget = [] map.set(target, cloneTarget) target.forEach((item, index) => { cloneTarget[index] = deepClone4(item, map)
6. 自定义new和instanceof工具函数
})
} else {
cloneTarget = {}
map.set(target, cloneTarget)
Object.keys(target).forEach(key => {
cloneTarget[key] = deepClone4(target[key], map)
})
}
return cloneTarget
} else {
return target
}
}
/*
自定义new工具函数
语法: newInstance(Fn, ...args)
功能: 创建Fn构造函数的实例对象
实现: 创建空对象obj, 调用Fn指定this为obj, 返回obj
*/
function newInstance(Fn, ...args) {
// 创建一个新的对象
const obj = {}
// 执行构造函数
const result = Fn.apply(obj, args) // 相当于: obj.Fn()
// 如果构造函数执行的结果是对象, 返回这个对象
if (result instanceof Object) {
return result
}
// 给obj指定__proto__为Fn的prototype
obj.__proto__ = Fn.prototype
// 如果不是, 返回新创建的对象
return obj
}
function Fn () {
this.name = 'tom'
return []
}
new Fn()
/*
自定义instanceof工具函数:
语法: myInstanceOf(obj, Type)
功能: 判断obj是否是Type类型的实例
实现: Type的原型对象是否是obj的原型链上的某个对象, 如果是返回true, 否则返回false
*/
function myInstanceOf(obj, Type) {
// 得到原型对象
let protoObj = obj.__proto__
7. 字符串处理 8. 简单排序: 冒泡 / 选择 / 插入
// 只要原型对象存在
while(protoObj) {
// 如果原型对象是Type的原型对象, 返回true
if (protoObj === Type.prototype) {
return true
}
// 指定原型对象的原型对象
protoObj = protoObj.__proto__
}
return false
}
/*
- 字符串倒序: reverseString(str) 生成一个倒序的字符串
- 字符串是否是回文: palindrome(str) 如果给定的字符串是回文,则返回 true ;否则返回 false
- 截取字符串: truncate(str, num) 如果字符串的长度超过了num, 截取前面num长度部分, 并以... 结束 */
/*
- 字符串倒序: reverseString(str) 生成一个倒序的字符串 */ function reverseString(str) { // abc // return str.split('').reverse().join('') // return [...str].reverse().join('') return Array.from(str).reverse().join('') }
/*
- 字符串是否是回文: palindrome(str) 如果给定的字符串是回文,则返回 true ;否则返回 false abcba abccba */ function palindrome(str) { return str === reverseString(str) }
/*
- 截取字符串: truncate(str, num) 如果字符串的长度超过了num, 截取前面num长度部分, 并以... 结束 abcde... */ function truncate(str, num) { return str.length > num? str.slice( 0 , num) + '...' : str }
/*
冒泡排序的方法
*/
function bubbleSort (array) {
// 1.获取数组的长度
var length = array.length;
// 2.反向循环, 因此次数越来越少 for (var i = length - 1 ; i >= 0 ; i--) { // 3.根据i的次数, 比较循环到i位置 for (var j = 0 ; j < i; j++) { // 4.如果j位置比j+1位置的数据大, 那么就交换 if (array[j] > array[j + 1 ]) { // 交换 // const temp = array[j+1] // array[j+1] = array[j] // array[j] = temp [array[j + 1 ], array[j]] = [array[j], array[j + 1 ]]; } } }
return arr; }
/* 选择排序的方法 */ function selectSort (array) { // 1.获取数组的长度 var length = array.length
// 2.外层循环: 从 0 位置开始取出数据, 直到length-2位置 for (var i = 0 ; i < length - 1 ; i++) { // 3.内层循环: 从i+1位置开始, 和后面的内容比较 var min = i for (var j = min + 1 ; j < length; j++) { // 4.如果i位置的数据大于j位置的数据, 记录最小的位置 if (array[min] > array[j]) { min = j } } if (min !== i) { // 交换 [array[min], array[i]] = [array[i], array[min]]; } }
return arr; }
/* 插入排序的方法 */ function insertSort (array) { // 1.获取数组的长度 var length = array.length
// 2.外层循环: 外层循环是从 1 位置开始, 依次遍历到最后 for (var i = 1 ; i < length; i++) { // 3.记录选出的元素, 放在变量temp中 var j = i
五. jQuery
1. 整体理解
1) jQuery是一个功能强大的函数库, 封装了以下功能
DOM的CRUD
DOM事件处理
ajax请求
各种小的工具函数
2) 核心语法
jQuery核心函数
jQuery核心对象
3) 特点:
链式调用
读写合一
更好的浏览器兼容性
2. jQuery核心函数
1) 是什么?
就是jquery库向外暴露的$和jQuery, 一般都用$
2) 它有 2 种使用方式:
作为函数使用
作为对象使用
3) $作为函数使用
参数为函数 $(() => {})
参数是选择器字符串 $('ul')
参数是标签格式字符串 $('
')
var temp = array[i]
// 4.内层循环: 内层循环不确定循环的次数, 最好使用while循环
while (j > 0 && array[j - 1 ] > temp) {
array[j] = array[j - 1 ]
j--
}
// 5.将选出的j位置, 放入temp元素
array[j] = temp
}
return array
}
参数是DOM元素 $(this)
4) $作为函数对象使用
发送ajax请求的各种方法
一些工具方法
3. jQuery核心对象
1) 2种情况产生的对象 (内部包含n个dom元素对象)
执行$函数返回的对象
调用jQuery核心对象的方法返回的对象
2) 主要方法
进行DOM进行增删改的方法
对内部的DOM元素进行进一步过滤查找的方法
进行事件处理的方法
直接绑定事件监听
事件委托
解绑事件监听
六-1. Vue
1. Vue相关API
1) 全局配置
Vue.config对象的属性
Vue.config.productionTip = false
2) 全局API
Vue.component():
Vue.directive():
Vue.filter():
Vue.use():
Vue.nextTick(callback):
Vue.set():
Vue.delete():
3) 配置选项
数据
data:
props:
computed:
methods:
watch:
DOM
el:
template:
render:
生命周期
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
beforeDestroy
destroyed
activated
deactivated
errorCaptured
资源
directives:
filters:
components:
杂项
mixins:
provide / inject:
其它
name:
functional
4) 实例属性
$el
$parent
$children
$refs
$attrs
$listeners
5) 实例方法
数据
$watch()
$nextTick()
$set()
$delete()
事件
$on()
$once()
$off()
$emit()
生命周期
$mount()
$nextTick()
$destroy()
6) 指令
v-text
v-html
v-show
v-if
v-else
v-else-if
v-for
v-on
v-once
v-bind
v-model
v-slot
7) 特殊属性
key
ref
is
8) 内置组件
component
transition
keep-alive
slot
面试题
v-if与v-show的区别
隐藏: v-if干掉标签, v-show通过样式来隐藏
重新显示: v-if需要重新创建标签对象, v-show只需要修改样式显示出来就可以
v-show重新显示更快, 但隐藏时还占用着内存空间: 以空间换时间
v-show更适合切换频繁/需要隐藏的DOM结构比较大
使用v-if解决模板中初始解析undefined的bug {{a.b.c}} a初始为一个空对象
computed与watch的区别
计算属性必须同步返回计算结果, 而watch中可以在异步操作后更新数据显示
watch可以深度监视, 计算属性只是监视了使用到的数据
选择:
如果是根据现在的数据同步计算就可以确定要显示的另一个数据 ==> computed
如果涉及到异步操作/深度监视 ==> watch
一旦一个数据变化, 我们需要做一系列操作 ===> watch
computed与method的区别
计算属性有缓存, 多次读取显示只计算一次
method, 多处显示计算多次
为什么不推荐v-for与v-if一起使用
对遍历的item数据进行限制判断
问题: 如果使用v-if, 每个数组元素都会解析指令来判断 ==> 效率低
解决: 不使用v-if, 使用计算属性, 过滤产生一个子数组 ==> 效率高
根据外部的数据判断
问题: 如果在当前标签上用v-if, 执行n次 ==> 效率低
解决: 添加一个/在父标签, 使用v-if, 执行 1 次 ==> 效率高
2. vue组件的生命周期
生命周期勾子(回调函数)本身什么事都没处理,
vue只是在做一些特定工作前后, 调用特定勾子函数来通知组件去做需要做事情
1) 单个组件生命周期
初始化:
beforeCreate: 不能通过this读取data数据和调用methods中的方法
执行了一些初始化的准备工作
created: 可以通过this读取data数据和调用methods中的方法
编译模板
beforeMount: 不能通过ref读取到页面中内容
挂载编译好的模板, 显示页面
mounted: 能通过ref读取到页面中内容
更新: this.msg += '--'
beforeUpdate (在数据更新后, 界面更新前调用): 读取的数据是最新的, 但页面是老的
更新页面
updated: 读取的数据和页面都是新的
死亡: $destroy()
beforeDestroy: 做一些收尾的工作, 比如: 清除定时器/解绑监听/...
死亡了
destroyed
发请求在哪里发?
一般在 mounted/created 都可以, mounted中会稍微晚一丁点
在一个特殊情况下不能在created中发: 如果在发请求前需要先读取一下页面中特定内容
2) 父子组件的生命周期
初始化:
beforeCreate
created
beforeMount
--child beforeCreate
--child created
--child beforeMount
--child mounted
mounted
更新:
beforeUpdate
--child beforeUpdate
--child updated
updated
死亡:
beforeDestroy
-- child beforeDestroy
-- child destroyed
destroyed
3) 带缓存的路由组件生命周期
activated: 初始化的最后 / 再回到当前路由
deactivated: 离开当前路由
初始化:
...
mounted
activated
路由离开
++beforeCreate
++created
++beforeMount
deactivated
++mounted
++activated
路由回来
++deactivated
activated
4) 捕获子组件错误的勾子
子组件执行抛出错误
errorCaptured return false代表错误已经处理了, 不需要再向外传递这个错误
5) 各个生命周期勾子说明
(1) beforeCreate(): 在实例初始化之后调用, data和methods都还没有初始化完成, 通过this不能访 问
初始化data与methods/computed
(2) created(): 此时data和methods都已初始化完成, 可以通过this去操作, 可以在此发ajax请求
编译模板
(3) beforeMount(): 模板已经在内存中编译, 但还没有挂载到页面上, 不能通过ref找到对应的标签对 象
插入到界面上显示
(4) mounted(): 页面已经初始显示, 可以通过ref找到对应的标签, 也可以选择此时发ajax请求
n次更新数据
(5) beforeUpdate(): 在数据更新之后, 界面更新前调用, 只能访问到原有的界面
更新界面
(6) updated(): 在界面更新之后调用, 此时可以访问最新的界面
销毁组件/ v-if隐藏/离开不缓存的路由组件
(7) beforeDestroy(): 实例销毁之前调用, 此时实例仍然可以正常工作
(8) destroyed(): Vue 实例销毁后调用, 实例已经无法正常工作了
(9) deactivated():组件失活, 但没有死亡
(10) activated(): 组件激活, 被复用
(11) errorCaptured(): 用于捕获子组件的错误,return false可以阻止错误向上冒泡(传递)
3. 组件深入
1) 动态组件
通过动态确定要显示的组件, is指定要显示组件的组件名
问题: 当从A组件切换到B组件时, A组件就会销毁
2) 缓存组件
使用缓存动态组件, 可以通过include或exclude指定只缓存特定组件
使用也可以缓存路由组件
路由组件对象什么时候创建?
默认: 每次跳转/访问对应的路由路径时
有缓存: 第一次访问时
路由组件对象什么时候死亡?
默认: 离开时
有缓存: 离开时不死亡, 只有当destroy/父组件死亡/刷新页面
<component :is="currentComp" />
<keep-alive :exclude="['Home']">
<component :is="currentComp"/>
</keep-alive>
<keep-alive include="Life1">
<router-view></router-view>
</keep-alive>
3) 异步组件
好处: 能更快的看到当前需要展现的组件界面(异步组件的代码开始没有加载)
无论是 路由组件 还是 非路由组件 都可以实现异步组件效果
拆分单独打包
需要时才请求加载组件对应的打包文件
配置组件: component: () => import(modulePath)
import(modulePath): 被引入的模块会被单独打包(code split) --ES8的新语法
() => import(modulePath): 函数开始不调用, 当第一次需要显示对应的界面时才会执行,
请求加载打包文件
细节
import()返回promise, promise成功的结果就是整体模块对象
本质上: 可以利用import()实现对任意模块的懒加载
4) 函数式组件: functional + render + JSX
函数组件的特点
无状态
无法实例化
内部没有任何生命周期处理函数
轻量,渲染性能高,适合只依赖于外部数据传递而变化的组件(展示组件,无逻辑和状态修
改)
可以没有根标签
编码
5) 递归组件
递归组件: 组件内部有自己的子组件标签
应用场景: 用于显示树状态结构的界面
注意: 递归组件必须有name
编码: 实现一个简单的可开关的树状结构界面的 Tree 组件
4. Vue组件间多种通信方式
通信方式列表:
export default {
functional: true, // 当前是函数组件
render (createElement, context) {
return 要显示界面的虚拟DOM
}
}
组件间关系
根据通信的 2 个组件间的关系来选择一种通信方式
1. props
1) props
2) vue自定义事件
3) 全局事件总线 / pubsub
4) v-model
5) .sync
6) $attrs与$listeners
7) $ref & $children & $parent
8) provide与inject
9) Vuex
10) 插槽 ==> 作用域插槽
父子
祖孙
兄弟
远亲(其它)
1) 父向子
props(非函数)
v-model
$refs, $children
插槽
2) 子向父
props(函数)
vue自定义事件
v-model
.sync
$parent
作用域插槽
3) 祖孙间
$attrs与$listeners
provide与inject
4) 兄弟或其它/任意
全局事件总线
Vuex
1) 实现父向子通信: 属性值是非函数
2) 实现子向父通信: 属性值是函数
3) 应用: 最基本, 用得最多的方式
2. vue自定义事件 3. 全局事件总线
消息订阅与发布(也就是pubsub)功能类似, 但在vue项目中不使用
4. v-model
1) 实现子组件向父组件通信
2) 相关语法:
父组件中绑定自定义事件监听:
<Child @eventName="callback($event)">
child.$on('eventName', callback)
子组件中分发事件
this.$emit('eventName', data)
3) 应用:
elment-ui的组件的事件监听语法都用的是自定义事件
我们项目中的组件也用了不少自定义事件
1) 实现任意组件间通信
2) 编码:
将入口js中的vm作为全局事件总线对象:
beforeCreate() {
Vue.prototype.$bus = this
}
传递数据的组件分发事件: this.$bus.$emit('eventName', data)
接收数据的组件处理监听: this.$bus.$on('eventName', (data) => {})
3) 应用:
前台项目中使用全局事件总线
5. .sync 6. $attrs与$listeners 7. $refs & $children & $parent
1) 实现父子之间相互通信/同步
2) 组件标签上的v-model的本质: 动态value属性与自定义input监听(接收子组件分发的数据更新父
组件数据)
父组件:
<CustomInput v-model="name"/>
<!-- 等价于 -->
<CustomInput :value="name" @input="name=$event"/>
子组件:
<input type="text" :value="value" @input="$emit('input',
$event.target.value)">
props: ['value']
3) 应用
element-ui中的表单项相关组件都用了v-model: Input / Select / Checkbox / Radio
1) 在原本父向子的基础上增加子向父
2) 组件标签的属性上使用.sync的本质: 通过事件监听来接收子组件分发过来的数据并更新父组件的数
据
父组件:
<child :money.sync="total"/>
<!-- 等价于 -->
<Child :money="total" @update:money="total=$event"/>
data () {
return {
total: 1000
}
},
子组件:
<button @click="$emit('update:money', money-100)">花钱</button>
props: ['money']
3) 应用:
element-ui在有显示隐藏的组件上: Dialog / Drawer
1) $attrs
实现当前组件的父组件向当前组件的子组件通信(祖孙间通信)
它是包含所有父组件传入的标签属性(排除props声明, class与style的属性)的对象
使用: 通过 v-bind="$attrs" 将父组件传入的n个属性数据传递给当前组件的子组件
2) $listeners
实现当前组件的子组件向当前组件的父组件通信 (孙向祖通信)
$listeners是包含所有父组件传入的自定义事件监听名与对应回调函数的对象
使用: 通过v-on="$listeners" 将父组件绑定给当前组件的事件监听绑定给当前组件的子组件
3) 应用
利用它封装了一个自定义的带hover文本提示的el-button
1) $refs
实现父组件向指定子组件通信
$refs是包含所有有ref属性的标签对象或组件对象的容器对象
8. provide与inject 9. Vuex 10. 插槽 ==> 作用域插槽slot-scope
使用: 通过 this.$refs.child 得到子组件对象, 从而可以直接更新其数据或调用其方法更新
数据
2) $children
实现父组件向多个子组件通信
$children是所有直接子组件对象的数组
使用: 通过this.$children 遍历子组件对象, 从而可以更新多个子组件的数据
3) $parent
实现子组件向父组件通信
$parent是当前组件的父组件对象
使用: 通过this.$parent 得到父组件对象, 从而可以更新父组件的数据
4) 应用
在后台管理项目中使用了$refs
1) 实现祖孙组件间直接通信
2) 使用
在祖组件中通过provide配置向后代组件提供数据
在后代组件中通过inject配置来声明接收数据
3) 注意:
不太建议在应用开发中使用, 一般用来封装vue插件
provide提供的数据本身不是响应式的 ==> 父组件更新了数据, 后代组件不会变化
provide提供的数据对象内部是响应式的 ==> 父组件更新了数据, 后代组件也会变化
方法二:
祖: 定义返回数据的方法, 通过provide提供这个方法
后代: 注入这个方法, 定义计算属性返回这个方法返回的数据
4) 应用:
element-ui中的Form组件中使用了provide和inject
1) 实现任意组件间通信
2) Vuex 是一个专为 Vue 应用程序设计的管理多组件共享状态数据的 Vue 插件
任意组件都可以读取到Vuex中store的state/getters对象中的数据
任意组件都可以通过dispatch()或commit()来触发store去更新state中的数据
一旦state中的数据发生变化, 依赖于这些数据的组件就会自动更新
3) 应用
前台和后台项目都有用vuex管理组件数据
1) 实现父组件向子组件传递标签内容
2) 什么情况下使用作用域插槽?
父组件需要向子组件传递标签结构内容
但决定父组件传递怎样标签结构的数据在子组件中
3) 编码:
子组件:
<slot :row="item" :$index="index"> <!-- slot的属性会自动传递给父组件 --
>
</slot>
父组件:
<template slot-scope="{row, $index}">
<template v-slot="{row, $index}">
<span>{{$index+1}}</span>
路由组件间通信方式?
5. Vue的响应式
1) 几个重要问题?
mvvm的理解, 与MVC的区别?
查看word
组件的data为什么只能是函数不能是对象?
同一个组件的多个组件实例的data必须是不同的对象(内容初始数据可以相同)
如果是data是对象, 组件的多个实例共用一个data对象
如果是函数, 组件对象通过调用函数得到的一个新的data对象
响应式数据与非响应式数据?
响应式: data / props / computed/ vuex的state与getters
非响应式: 仅仅存在于组件对象上的属性数据
<span :style="{color: $index%2===1? 'blue' : 'green'}" >
{{row.text}}</span>
</template>
4) 应用
element-ui中绝大部分组件都用了插槽
element-ui中的 table-column 组件使用了作用域插槽
query参数
params参数
props(需要配置, 而不是标签属性)
meta(也是配置)
vuex
data: {
msg: 'abc'
}
data () {
return {msg: 'abc'}
}
a1 = new VueComponent()
a2 = new VueComponent()
a1._data = A.data()
a2._data = A.data()
obj1 = {m:1}
obj2 = {m:1}
obj1===obj2
给组件对象添加一个新属性: this.xxx = value
直接给一个响应式对象添加一个新属性: this.product.xxx = 'abc' ==>
this.$set(this.product, 'xxx', 3)
对象的响应式与数组的响应式有什么区别?
对象: 通过Object.defineProperty()添加setter方法来监视属性数据的改变 + 订阅-发布
数组: 重写更新数组元素的一系列方法 + 订阅-发布
2) Vue数据绑定/响应式原理图
理解:
说vue的数据绑定的原理, 或者数据响应式的原理, 都是在说一个事
当我们修改了data中的数据时, 组件界面是如何自动更新的
这里涉及下面几个重点
数据代理: Object.defineProperty() this.msg = 'abc' ==> data.msg = 'abc'
数据劫持/监视: Object.defineProperty()
发布-订阅: observer 与 dep 与 watcher
this._data.msg = 'xxx'
数据代理
通过Object.defineProperty()给vm添加与data对象中对应的属性
在getter中, 读取data中对应的属性值返回 ==> 当我们通过this.xxx读值时, 读取的是
data中对应的属性值
在setter中, 将最新的值保存到data中对应的属性上 ==>当我们通过this.xxx = value时,
value保存在data中对应的属性上
作用: 简化对vm/组件对象内部的data对象的属性数据的操作(读/写)
数据劫持/监视
在observer中, 通过Object.defineProperty()给data中所有层次属性都添加上
getter/setter
为每个属性都创建一个dep对象, 用于后面更新
注意: 在解析模板时, 为每个表达式都创建了一个用于更新对应节点的watcher
在getter中, 去建立dep与watcher之间的关系
dep与data中的属性一一对应
watcher与模板中的表达式一一对应
一个dep中, 保存了包含n个watcher的数组 ==> 当多个表达式用到当前dep所对应
的属性
一个watcher中, 保存了包含n个dep的对象 ==> 当表达式是一个多层的表达式
在setter中, 通过dep去通知所有watcher去更新对应的节点
发布-订阅模式
发布者: observer
订阅者: watcher: 订阅数据的变化==> 一旦数据变以, 得告诉我, 我来负责做更新节点的
操作
订阅器/中间人: dep
初始化: 一个data中的属性 ==> dep ==> 对应n个watcher ==> 第个watcher都有一个更
新对应节点的函数
更新: this.name = 'xxx'
由于有数据代理的存在 ===> data中的name属性更新了
由于有数据劫持/监视的存在 ===> observer中name属性对应的setter方法调用了
利用发布-订阅模式机制 ==> 由name对应的deep对象来通知所有对应的watcher去
更新对应的节点
初始化
实现数据代理: 通过defineproperty给vm/组件对象添加与data中对应的属性
在getter中读取data中对应属性返回
在setter中将最新的value保存到data对应的属性上
创建oberver(发布者):
使用defineProperty来劫持/监视data中所有层次属性
为data中每个属性创建对应的dep对象(订阅器) ==> 用于后面界面更新
创建compile
编译模板, 实现界面的初始化显示
为每个包含非事件指令表达式的节点创建对应的watcher
绑定用于更新界面的回调函数
将watcher(订阅者)添加到dep(订阅器)中去
更新数据后的基本流程
this.xxx = value
由于有数据代理, data中的xxx会更新
由于有数据劫持, xxx对应的setter就会调用
在setter中, 通过dep去通知所有对应的watcher去更新对应的节点
3) Vue双向数据绑定
通过v-model来实现双向数据绑定
v-model的本质
将动态的data数据通过value属性传给input显示 ==> data到view的绑定
给input标签绑定input监听, 一旦输入改变读取最新的值保存到data对应的属性上 ==>
view到data的绑定
双向数据绑定是在单向数据绑定(data-->view)的基础, 加入input事件监听(view ==> data)
4) 响应式原理面试交流总结
初始化
实现数据代理
通过defineproperty给vm定义与data中属性对应的带getter/setter的属性
在getter中, 读取data中对应的属性值返回 ==> 读取this.msg ==> 读取的是data
中msg属性值
在setter中, 将最新值保存到data对应的属性上 ==> this.msg = 'abc' ==> 'abc'会
保存到data的msg上
创建observer
目标: 对data中所有层次的属性进行监视/劫持
通过defineproperty给data中所有层次属性, 都重新定义, 加上getter与setter
getter: 用来建立dep与watcher的关系
setter: 用来当data数据发生改变去更新界面
为data中所有层次的属性创建一个对应的dep ==> 用来将来更新界面的
创建compile
目标1: 实现界面的初始化显示
目标2: 为将更新做准备
为模板中每个包含表达式(事件表达式除外)的节点创建一个对应的watcher
给watcher绑定用于更新对应节点的回调函数
将watcher添加到n个对应的dep中
更新
this.msg = 'abc'
由于有数据代理 ==> data的msg更新为了'abc'
由于有数据劫持 ==> data中msg的setter调用了
在setter中, 通过对应的dep去通知所对应的watcher去更新对应的节点 ==> 使用了订
阅发布模式
6. 可复用性
1) mixin(混入)
用来复用多个组件中相关的js代码的技术
将多个组件相同的js代码提取出来, 定义在一个mixin中配置对象
在多个组件中通过mixins配置引入mixin中的代码, 会自动合并到当前组件的配置中
2) 自定义指令
Vue.directive('upper-text', (el, binding) => {
el.innerText = binding.value.toUpperCase()
})
<p v-upper-text="msg"></p>
msg: 'I Will Back'
3) 自定义过滤器
4) 自定义插件
问题: Vue.use()内部做了什么?
对象插件: 调用插件对象install方法(传入Vue)来安装插件(执行定义新语法的代码)
函数插件: 直接将其作为install来调用(传入Vue)来安装插件(执行定义新语法的代码)
7. Vue状态管理: Vuex
1) vuex的 5 大属性
state
mutations
actions
getters
modules
2) vuex的数据流结构图
// 注册全局过滤器
Vue.filter('date-format', (value) => {
// return moment(value).format('YYYY-MM-DD HH:mm:ss')
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
})
<p>{{startTime | date-format}}</p>
// 对象插件
const myPlugin = {
// 必须有此方法
install (Vue) {
// 通过Vue来扩展新的功能语法, 如注册全局组件/指令/过滤器/...
}
}
// 函数插件
const myPlugin = (Vue) => {
// 通过Vue来扩展新的功能语法, 如注册全局组件/指令/过滤器/...
}
export default myPlugin
// 在入口JS中引入, 并声明使用来安装插件
import myPlugin from './vue-myPlugin'
Vue.use(myPlugin)
3) vuex多模块编程
vuex的多模块编程的必要性
vuex单模块问题:
需要的管理状态数据比较多, 那对应的mutations/actions模块就会变得比较大
如果添加新的数据管理, 需要修改现在文件(不断向其添加内容)
vuex多模块编程: 对各个功能模块的数据分别进行管理, 这样更加具有扩展性
什么时候需要用vuex多模块编程? 需要vuex管理的数据比较多时使用
多模块编程的总state结构:
4) 问题1: vuex中的mutation可以执行异步操作吗?
可以 ==> 异步更新数据后界面确实会自动更新
问题 ==> vuex的调用工具监视不到mutation中的异步更新, 工具记录还是更新前的数据(不对)
扩展: 工具如何记录数据变化? ==> 每次mutation函数执行完后, 立即记录当前的数据 ==> 在
mutation中同步更新state, 才能被记录到
5) 问题2: vuex中的状态数据的响应式的原理?
1. 创建了一个vm对象
2. state中的数据都是vm的data数据(是响应式的)
3. 组件中读取的state数据本质读取的就是data中的数据
4. 一旦更新了state中的数据, 所有用到这个数据的组件就会自动更新
{
home: {
categoryList: [],
xxx: {}
},
user: {
userInfo: {}
}
}
6) 问题3: vuex数据刷新丢失的问题
绑定事件监听: 在卸载前保存当前数据
在初始时读取保存数据作为状态的初始值
8. Vue路由: vue-router
1) 一些基本知识
跳转/导航路由的 2 种基本方式
声明式路由: xxx</router-link/>
编程式路由: this.$router.push/replace(location)
跳转路由携带参数(数据)的方式
params参数
注册的路由路径得有占位:
name: 'xxx',
path: '/xxx/:name/:age'
跳转时指定参数值:
/xxx/abc/12
{name: 'xxx', params: {name: 'abc', age: 12}}
读取数据
this.$route.params.name/age
query参数
?后面的参数 /xxx?name=tom&age=12
注册路由时不需要做特别的指定
读取数据
this.$route.query.name/age
props
props: true, // 只能同名映射params参数
props: {a: 1, b: 'abc'}, // 只能映射非params/query参数
props: route => ({keyword3: route.params.keyword, keyword4:
route.query.keyword2, xxx: 12}), //可以指定任何数据都可以
meta
通过路由的meta的配置来指定包含n个数据的对象
window.addEventListener('beforeunload', () => { // 当页面刷新时, 页面卸载前的事件
回调
sessionStorage.setItem('CART_LIST_KEY',
JSON.stringify(this.$store.state.shopCart.cartList))
})
window.removeEventListener('beforeunload')
cartList: JSON.parse(sessionStorage.getItem('CART_LIST_KEY')) || [],
{
meta: { isHideFooter: true }
}
取数据: this.$route.meta.isHideFooter
location的 2 种类型值: push/replace(location)
字符串 path
对象形式: {name, path, params, query}
push/replace(location)
2) 参数相关问题
params与path配置能不能同时使用
不可以: router.push({path: '/xx', params: {name: 'tom'}})
params只能与name配合: router.push({name: 'xx', params: {name: 'tom'}})
如何配置params参数可传可不传?
path: '/search/:keyword?',
注意: 一旦声明可以不传, 不能传入一个空串的param参数
push({name: 'search', params: {keyword: ''}}) // 不允许
push({name: 'search'})
push({path: '/search'})
push('/search')
跳转携带的参数, 刷新就丢失了
如果注册没有指定/:xxx的点位, 而跳转时通过params配置携带的参数数据, 刷新时就会丢失
因为url中没有携带的参数数据路径
name: 'user'
/user/:id
/user
this.$router.push({name: 'user', params: {id: 2}})
this.$route.params.id
/user
路由组件能不能传递props参数?
可以, 但只是将params/query/其它映射成props传入路由组件的
编程式路由跳转到当前路由, 参数不变, 会报出错误? ==> 在做项目时有没有遇到比较难/奇怪
的问题?
push(location, () => {}, () => {}) 老的方式 3.1.0
push(location).then(() => {}, () => {}) 新的方式 3.1.0
说明情况:
上一个项目这种操作没有这个问题
后面的一个项目(2019.8之后)开始有这个问题, 而且是声明式跳转没有, 只有编程式跳转有
当编程式跳转到当前路由且参数数据不变, 就会出警告错误:
错误: Avoided redundant navigation to current location ==> 重复跳转当前路由
原因:
vue-router在3.1.0版本(2019.8)引入了push()的promise的语法, 如果没有通过参数指定
回调函数就返回一个promise来指定成功/失败的回调, 且内部会判断如果要跳转的路径
和参数都没有变化, 会抛出一个失败的promise
说明文档: https://github.com/vuejs/vue-router/releases?after=v3.3.1
解决:
办法1: 在每次push时指定回调函数或catch错误
办法2: 重写VueRouter原型上的push方法 (比较好)
1). 如果没有指定回调函数, 需要调用原本的push()后catch()来处理错误的
promise
2). 如果传入了回调函数, 本身就没问题, 直接调用原本的push()就可以
说明:
声明式路由跳转之所有没有问题, 是因为默认传入了成功的空回调函数
3) 有点难度, 但很重要的
路由懒加载: ===> 预加载(vue脚手架项目已经实现了 ==> webpack)
() => import('./Home.vue')
组件单独打包, 开始不加载其打包文件, 第一次请求时才会加载 ==> 加载更快, 提高用户
体验
缓存路由组件
push('/xxx', () => {}) ===> 声明式路由跳转本质就是这样执行的
push('/xxx').catch()
const originPush = VueRouter.prototype.push
VueRouter.prototype.push = function (location, onComplete, onAbort)
{
console.log('push()', onComplete, onAbort)
// 判断如果没有指定回调函数, 通过call调用源函数并使用catch来处理错误
if (onComplete===undefined && onAbort===undefined) {
return originPush.call(this, location).catch(() => {})
} else { // 如果有指定任意回调函数, 通过call调用源push函数处理
return originPush.call(this, location, onComplete, onAbort)
}
}
路由离开时不销毁, 路由回来时不用重新创建 ==> 利用缓存, 切换路由更快
再利用上prefetch/preload实现预获取/加载, 用户体验更佳
动态添加路由
router.addRoutes(routes)
在异步确定用户的权限路由后, 需要动态添加到路由器
路由守卫与权限校验
router.beforeEach()注册全局前置守卫
统一对用户权限进行一系列的校验处理
history与hash路由的区别和原理
区别:
history: 路由路径不#, 刷新会携带路由路径, 默认会出 404 问题, 需要配置返回首页
404:
history有: 刷新请求时会携带前台路由路径, 没有对应的资源返回
hash没有: 刷新请求时不会携带#路由路径
解决:
开发环境: 如果是脚手架项目本身就配置好
==> webpack ==> devServer: {historyApiFallback : true}
当使用 HTML5 History API 时, 所有的 404 请求都会响应 index.html
的内容
生产环境打包运行:
配置nginx
hash: 路由路径带#, 刷新不会携带路由路径, 请求的总是根路径, 返回首页, 没有 404
问题
原理:
history: 内部利用的是history对象的pushState()和replaceState() (H5新语法)
hash: 内部利用的是location对象的hash语法
写hash路径 location.hash = '#/xxx'
读hash路径: location.hash
监视hash路径的变化: window.onhashchange = () => {}
如何让路由跳转后, 滚动条自动停留到起始位置?
<keep-alive>
<router-view/>
</keep-alive>
location / {
try_files $uri $uri/ /index.html; # 所有 404 的请求都返回
index页面
}
new VueRouter({ // 配置对象
scrollBehavior (to, from, savedPosition) {
// 对于所有路由导航,让页面滚动到顶部。
// return { x: 0, y: 0 }
如何实现登陆后, 自动跳转到前面要访问的路由界面
在全局前置守卫中, 强制跳转到登陆页面时携带目标路径的redirect参数
在登陆成功后, 跳转到redirect参数的路由路径上
4) 路由导航守卫的理解和使用
导航守卫是什么?
导航守卫是vue-router提供的下面 2 个方面的功能
监视路由跳转 -->回调函数
控制路由跳转 --> 放行/不放行/强制跳转到指定位置 next()
应用
在跳转到界面前, 进行用户权限检查限制(如是否已登陆/是否有访问路由权限)
在跳转到登陆界面前, 判断用户没有登陆才显示
导航守卫分类
全局守卫: 针对任意路由跳转
全局前置守卫
全局后置守卫
router.afterEach((to, from) => {})
路由守卫
// 在按下 后退/前进 按钮时, 页面滚动停留在离开时的位置
if (savedPosition) {
return savedPosition // 返回前面离开时保存的位置坐标对象
} else { // 否则直接滚动到顶部
return { x: 0 , y: 0 }
}
}
})
if (userInfo.name) {
next()
} else {
// 如果还没有登陆, 强制跳转到login
next('/login?redirect='+to.path) // 携带目标路径的参数数据
}
await this.$store.dispatch('login', {mobile, password})
// 成功了, 跳转到redirect路由 或 首页
const redirect = this.$route.query.redirect
this.$router.replace(redirect || '/')
router.beforeEach((to, from, next) => {
// ...
})
前置守卫
{
path: '/xxx',
component: Foo,
beforeEnter: (to, from, next) => {}
}
组件守卫: 只针对当前组件的路由跳转
进入
更新:
beforeRouteUpdate (to, from, next) {}
离开
beforeRouteLeave (to, from, next) {}
六-2. 前台管理项目总结
1. 功能模块
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {}
},
{
path: '/ff',
component: Foo,
},
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
next(vm => {
// 通过 `vm` 访问组件实例
})
},
2. 使用的库 3. Vue配置 4. 2个重要方法: 5. 与后台交互
首页
商品搜索列表
商品详情
购物车
登陆与注册
订单交易/结算
支付
个人中心/订单列表
vue
vue-router
vuex
vee-validate
vue-lazyload
element-ui
axios
mockjs
nprogress
uuidjs
swiper
qrcode
lodash
name
data
props
computed
watch
methods
mounted/created
beforeCreate
components
vm.$nextTick()与Vue.nextTick()
vm.$set()与Vue.set()
$set(): 给响应式对象添加一个响应属性, 直接通过.xxx的方式添加, 这个属性不是响应式的, 界面
不会自动更新
$nextTick(callback): 一般是在数据更新之后, 界面更新前调用, 回调函数延迟到界面更新后执行
6. vuex 7. 路由相关 8. 自定义通用型/复用型组件的基本步骤
axios二次封装
1). 配置通用的基础路径和超时
2). 显示请求进度条
3). 成功返回的数据不再是response, 而直接是响应体数据response.data
4). 统一处理请求错误, 具体请求也可以选择处理或不处理
5). 每次请求都携带一个userTempId请求头, 数据值在state中
6). 每次请求(已登陆)都携带一个token请求头, 数据值在state中
postman与测试接口
定义接口请求函数模块
在vuex的异步action中调用接口请求函数
将API挂载到Vue的原型对象上, 在组件中调用接口请求函数与后台交互: Vue.prototype.$API =
API
当后台接口还未完成时, 先mock数据, 可以使用mockjs, 当然有的公司可能有自己的一套
基本组成: store / state / mutations / actions / getters / modules
vuex多模块编程: 当要管理的数据过多时, 将每个模块的数据单独管理, 更方便, 更有扩展性
带vuex的整体编码组成:
component:
触发action调用请求获取数据: dispatch()
读取vuex中的数据: mapState() / mapGetters()
模板动态显示数据: 插值与指令
vuex:
action: 调用API函数请求获取数据, 提交给mutation
mutation: 更新state数据
api:
axios二次封装
定义针对不同接口的请求函数
跳转路由的 2 种基本方式: 声明式路由与编程式路由
跳转路由携带参数的类型: params与query参数
携带参数的 2 种方式: 字符串与对象形式
参数相关问题:
params与path配置能不能同时使用
如何配置params参数可传可不传?
路由组件能不能传递props参数
编程式路由跳转到当前路由, 参数不变会报出错误
如何让路由跳转后, 滚动条自动停留到起始位置
scrollBehavior配置
路由导航守卫的理解和使用
全局前置守卫
路由/组件前置守卫
9. 相关问题(具体内容看笔记,编写简历项目时可使用) 10. 面试可说性强的功能
1) 三级分类列表
使用编程式导航代替声明式导航
router-link太多 ==> 创建很多组件对象 ==> 占用内存大, 效率低
优化事件处理效率
利用事件委托: event.target
理解事件委托与事件冒泡
如何携带点击的分类的数据?
event.target得到a标签
实现静态组件: 模板/样式写好
设计从外部接收的数据: props
设计内部的数据: data
设计基于props和data的计算属性数据: computed
根据props和data数据和computed进行动态显示
更新数据, 更新界面, 通知父组件
编程式路由跳转到当前路由, 参数不变会报出错误
vue-router版本(3.1.0)变化: 上一个项目没问题, 新的项目有问题
重写VueRouter原型上的push/replace方法
优化减小打包文件:
对UI组件库实现按需打包
对lodash库实现按需引入
什么时候需要使用编程式导航代替声明式导航
有一定条件限定
个数太多
优化事件处理效率:
利用事件委托
如何携带点击的分类的数据?
利用自定义的data标签属性来保存分类信息
event.target.dataset得到a标签上的data自定义属性数据
对mouseEnter高频事件进行节流处理
使用lodash的throttle进行节流处理
解决swiper动态页面轮播的bug
watch + $nextTick()
解决Floor组件中轮播有问题的bug
watch的immediate
分发异步action后, 如何能知道处理完成了
await this.$store.dispatch('getProduct')
// 请求得到product后才执行下面的代码
回调函数
dipatch()的返回值是异步action返回的promise
区别userTempId与Token
userTempId: 未登陆的标识 userTempId: cartList
Token: 登陆用户的标识 token ==> userId: cartList
2 种懒加载的优化手段:
路由组件懒加载
图片懒加载
利用自定义的data标签属性来保存分类信息
对mouseEnter高频事件进行节流处理
使用lodash的throttle进行节流处理
对lodash库实现按需引入
2) 商品搜索列表
跳转路由携带条件数据
params参数: 搜索关键字
query参数: 分类id和名称
实现params参数可传可不传: /search/:keyword?
准备各种搜索条件
category1Id: '', // 一级分类ID
category2Id: '', // 二级分类ID
category3Id: '', // 三级分类ID
categoryName: '', // 分类名称
keyword: '', // 关键字
trademark: '', // 品牌 "ID:品牌名称"
props: [], // 商品属性的数组: ["属性ID:属性值:属性名"] 示例: ["2:6.0~6.24英寸:屏幕尺
寸"]
order: '1:desc', // 排序方式 1: 综合,2: 价格 asc: 升序,desc: 降序 示例: "1:desc"
pageNo: 1, // 当前页码
pageSize: 10, // 每页数量
添加搜索条件
删除搜索条件
删除分类或关键字条件后, 地址栏还有条件参数?
删除关键字条件, 输入框中的关键字没有同步删除?
排序处理
根据综合/价格/销量/评价排序
升序/降序
搞清条件数据结构
orderFlag:orderType
orderFlag: 1: 综合,2: 价格, 3: 销量, 4: 评价
orderType: asc: 升序,desc: 降序
删除搜索关键字条件, 清除输入框中的关键字 ===> 全局事件总线
删除分类条件/关键字条件 ==> 直接发请求
问题: 地址栏上的条件参数没有删除
解决: 重新跳转到search, 不再携带要删除条件参数, search组件监视路由的变化, 发请求
获取数据
3) 注册流程
前台: 输入注册需要的相关信息(用户名/密码/...), 进行前台表单校验, 如果不通过, 提示错误
前台: 发送注册的ajax请求(post), 携带注册接口需要的相关数据(用户名/密码/...)
后台: 获取到注册请求携带的参数, 去数据库中判断是否已经存在
如果已经存在, 返回提示此用户已存在的提示信息
如果不存在, 保存到数据库, 返回成功的数据
前台: 接收到响应
如果是不成功的数据, 提示
如果是成功的数据, 自动跳转到登陆页面
4) 登陆流程
前台: 输入登陆需要的相关信息(用户名/密码), 进行前台表单校验, 如果不通过, 提示错误
前台: 发送登陆的ajax请求(post), 携带登陆接口需要的相关数据(用户名/密码)
后台: 获取到登陆请求携带的参数, 去数据库中查询看是否存在
如果不存在, 返回登陆失败的信息
如果存在, 生成一个新的token字符串, 将token与用户信息一起返回
前台: 接收到响应
如果是不成功的数据, 提示
如果是成功的数据,
将用户信息和token都保存到vuex中
将token保存到localStorage中 ==> 不保存用户信息
跳转到首页或redirect页面
5) 自动登陆流程
简单说:
前台: 页面一加载时, 发送请求根据token获取用户信息
后台: 得到请求头中的token值, 解析出里面包含的userId和失效时间,
已经失效了: 返回代表token失效的 401 的错误响应
还在有效期内: 根据userId查询数据库, 得到用户信息返回
利用全局前置守卫:
一旦发现当前没有登陆, 但前面登陆过(有token, 没有用户信息)
发送请求根据token获取用户信息
成功了, 保存用户信
失败了(说明token过期了): 清除token, 强制跳转到登陆页面
6) 购物车模块
购物车数据是保存在后台的, 标识是什么?
未登陆: 标识为用户临时ID(userTempId)
第一次访问时前台利用uuid库生成的唯一字符串, 保存保存在local中
每次请求时通过请求头自动携带它(利用请求拦截器)
登陆: 登陆用户对应的token
用户请求登陆时, 服务器端生成并返回给浏览器, 浏览器收到后自动保存到local中
每次请求时通过请求头自动携带它(利用请求拦截器)
进入购物车页面, 显示购物车列表
根据临时用户ID或者token请求接口得到购物车列表数据显示
添加购物车 / 修改购物项数量
提交请求时, 携带商品的skuid和数量(变化的)
这 2 个操作是同一个接口, 也就是数量有可能是负数
获取购物车列表
请求获取时不需要携带额外参数, 会自动携带标识: userTempId / token
一旦用户登陆上, 后台会将关联在usertempId上的购物车数据合并到token对应用户关联
的购物车数据
删除购物项(一个/多个)
请求接口, 携带一个skuId或多个skuId的数组
参数: skuId的数组 [2, 3]
勾选购物项(一个/多个)
请求接口, 携带一个skuId或多个skuId的数组 和 是否勾选的标识数据(0/1)
7) 下单支付流程
去结算 ==> 获取订单交易数据
提交订单 ==>提交下单请求, 得到订单ID
根据订单ID获取支付信息
金额
支付url
支付
根支付Url生成支付二维码图片显示, 使用qrcode
扫码支付
轮询请求获取订单状态
分页显示订单列表
六-3. 后台管理项目总结
1. 商品管理功能
2. 使用的库
3. Vue配置
分类查询
品牌管理
平台属性管理
SPU管理
SKU管理
vue
vue-router
vuex
element-ui
axios
nprogress
lodash
js-cookie ==> localStorage
4. 2个重要方法: 5. 组件间通信 6. element-ui
name
data
props
computed
watch
methods
mounted/created
components
vm.$set()与Vue.set()
vm.$nextTick()与Vue.nextTick()
父向子通信 / 父组件更新子组件的数据
非函数props
$refs
$children
子向父通信 / 子组件更新父组件的数据
函数props
自定义事件
$parent
父子双向通信
v-model
.sync
祖孙间通信
$attrs & v-bind
$listeners & v-on
兄弟间通信
全局事件总线
vuex
任意间通信
全局事件总线
vuex
Card: 卡片效果的块, 很简单
Table: 列表表格组件, 最常用最复杂的组件之一
Form: 表单组件, 包括数据收集与表单校验, 最常用最复杂的组件之一
Select: 选择器, 基本的表单项组件
Input: 单行输入, 基本的表单项
Button: 按钮组件, 简单的基础的
Popconfirm: 气泡确认框, 常用于删除操作的确认提示
Tag: 标签, 比较简单
Pagination: 分页组件, 与我们自定义的分页组件类似
Drawer: 抽屉组件, 用得相对少些, 不太复杂
Dialog: 对话框组件, 最常用最复杂的组件之一
Upload: 文件上传组件, 封装度高, 最复杂的组件之一
MessageBox: 消息警告框与确认框
Message: 消息提示
Row/Col: 行列布局
7. 与后台交互 8. 数据处理/整理 9. 项目开发中数组常用方法
项目技能点
Carousel: 轮播组件
axios二次封装
swagger接口文件与测试接口
定义接口请求函数模块
整合所有接口请求函数模块并暴露
将API挂载到Vue的原型对象上
在组件中调用接口请求函数与后台交互
请求得到的数据: 整理后才保存到状态用于显示 ==> SpuForm的spuImageList
组件间传递对象数据: 需要进行对象的浅拷贝或深拷贝 ==> 平台属性修改的取消功能
在用户操作过程中: 需要将数据收集到特定对象或数组中, 用于提交请求的参数
在提交请求前: 需要对收集的数据进行整理后才能完全满足接口参数的要求
spuImageList/spuSaleAttrList
forEach()
map()
filter()
reduce()
find()
some()
every()
splice()
- elementUI 模板项目: vue-admin-template element-ui快速搭建项目界面 按需引入打包, 并解决PopConfirm背景透明的问题
- 深度作用选择器修改第三方UI组件的内部样式 作用域(scoped)样式 深度作用选择器 如: 修改轮播指标器样式 / 抽屉上下滑动
- 深克隆 使用lodash库的cloneDeep 功能: 平台属性修改取消 / 权限管理中
- $nextTick与$set 界面更新后执行: $nextTick() 给响应式对象添加新属性: $set() 功能: 列表项动态显示输入框并自动获得焦点
- 路由权限实现的 2 个重要技术 路由全局前置守卫 动态添加路由: addRoutes()
- 路由组件懒加载: const Foo = () => import('./Foo.vue')
import()动态引入路由组件 ==> 路由组件模块单独打包 函数初始不执行, 请求对应路由才执行 ==> 第一次请求时才加载打包文件
- 路由跳转处理 路由跳转的loading处理: nprogress 登陆成功自动跳转到redirect参数路由
- 组件间通信 自定义事件 CategorySelector SpuForm v-model el-input el-select .sync 控制SpuForm的隐藏 Dialog $attrs与v-bind $listeners与v-on 封装HintButton ref SpuForm/SkuForm数据初始化加载 插槽 默认插槽 通过标签体向子组件传入标签结构 Table / Form/ Upload/... 命名插槽 通过标签体向子组件传入多种不同的标签结构 Dialog / Upload 作用域插槽 决定父组件传递什么标签结构的数据在子组件中 el-table-column vuex 集中管理状态数据 多模块编程 user routes app
- 前后台交互 swagger: 接口文档与接口测试 async & await axios基本使用与axios二次封装 请求拦截器与响应拦截器 token保存与读取携带 js-cookie: 操作cookie数据 使用localStorage代替cookie 请求跨域--proxy/代理服务器 mockjs: 模拟/mock数据
- 项目功能数据整理/处理 请求获取的数据显示之前进行必要的整理 在交互过程中, 收集整理数据 发送请求前, 对收集的参数数据进行必要的整理
- 封装高复用组件 见下图
六-4. Vue项目优化
1. Vue代码层面优化
1) v-for 遍历列表
指定非下标的唯一key
不同时使用 v-if
v-if是在解析v-for产生的每个标签上解析
问题: 每个遍历产生的元素都会解析v-if, 解析的次数比较多
情况1: v-if判断的条件数据是item数据 ===> 利用计算属性直接过滤出要显示的子列表数据, 不 使用v-if
情况2: v-if判断的条件数据是另一个数据(与列表数据无关) ===> 在v-for的外层标签使用v-if(解 析一次)
2) 图片资源懒加载
如使用vue-lazyload
3) 路由组件懒加载
const Home = () => import('./pages/Home')
4) 第三方插件的按需引入打包
如: element-ui / vant
5) 大数组优化1: 冻结响应式数据
当前组件如果只是为纯展示组件时,拿到数据后使用Object.freeze()将数据冻结,这样数据就
无法进行响应变化。
6) 大数组优化2: 虚拟列表
当组件处于非常长的列表时,数据过多导致DOM元素同样多,导致卡顿。
使用业界常用手段虚拟列表,只渲染可以看到的窗口的区域DOM。
虚拟列表的基本实现思路: ==> 测试代码 vue/虚拟列表/index.html
用vue的for循环渲染列表,自己手动加一个滚动条,然后通过监听scroll,
算出应该显示到第几个,通过计算属性截取显示的数据
真实项目可以使用第三方插件:
vue-virtual-scroll-list
7) 事件销毁
Vue 组件销毁时,实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销 毁。单独添加的监听事件是不会移除的,需要手动移除事件的监听,以免造成内存泄漏。
2. webpack配置层面优化
1) 兼容性处理
JS
babel-loader: presets: ['@babel/preset-env'] 问题就是只能编译/转换简单语法
@babel/polyfill: 做复杂语法(新的APi)兼容,问题是体积太大了
core-js: 在@babel/preset-env基础上,增加了useBuiltIns: 'usage'来实现按需打包
CSS
export default {
data: () => ({
users: []
}),
async created() {
const users = await axios.get("/api/users");
this.users = users // 每个user对象中的属性都添加了setter监视
this.users = Object.freeze(users); // 这样数组内部就没有做数据劫持处理, 效率更
高
}
};
created() {
document.addEventListener('scroll', this.onScroll, false);
},
beforeDestory() {
document.removeEventListener('scroll', this.onScroll, false);
}
postcss-loader
内部使用autoprefixer插件, 给C3样式自动添加厂商前缀
在package.json中指定browserslist来指示postcss-loader兼容性做到什么程度
2) 拆分打包与压缩
对第三方JS包, css进行拆分打包
3) 资源预加载(prefetch)
4) 生产环境时不生成 SourceMap
productionSourceMap: false
减少打包文件
5) 文件名hash化=>利用浏览器缓存
对打包文件名用上contenthash ==> 某个bundle对应的模块文件内容发生改变文件名才会变化 ===> 利用浏览器缓存
6) 代码Tree Shaking
效果: 打包时'摇掉'模块中没有被使用的代码
条件: 必须是ES6模块化导出且进行代码压缩时
3. 基础的Web技术层面的优化
1) 开启 Gzip
下载: yarn add compression-webpack-plugin --dev
vue.config.js
nginx.conf中
var CompressionWebpackPlugin = require('compression-webpack-plugin');
...
configureWebpack: config => {
config.plugins.push(
new CompressionWebpackPlugin({
test: new RegExp('\\.(js|css)$'),
threshold: 8192,
minRatio: 0.8
})
)
2) 静态资源(css/js/img)使用CND引入
浏览器从服务器上下载 CSS、js 和图片等文件时都要和服务器连接,而大部分服务器的带宽有限, 如果超过限制,网页就半天反应不过来。而 CDN 可以通过不同的域名来加载文件,从而使下载文 件的并发连接数大大增加,且CDN 具有更好的可用性,更低的网络延迟和丢包率 。
六-5. TS & Vue3
TS
1. 区别TS与JS?(说说TS的特点)
强类型, 声明变量时可以指定特定类型, 编码时可以有更友好的提示(错误或补全), 易于写出更
健壮的程序
TS支持JS的所有语法特性, 也扩展了一些新的数据类型
新的基本类型: 枚举, 元组, 联合类型
新的复杂类型: 接口, 泛型
TS浏览器是不能直接运行的, 需要编译为JS才能运行
2. 说说你对接口的理解
接口是对状态(属性)或行为(方法)的抽象(描述)
接口可以用来约束一个对象/函数/类
3. 说说你对泛型的理解
泛型: 代表不确定的类型
泛型可以用在函数/接口/类上
泛型的 3 个操作
定义泛型类型
函数: 定义函数的函数名的右侧
接口: 定义函数的函数名的右侧
类: 定义函数的函数名的右侧
使用泛型: T
函数: 参数/返回值/函数体
接口: 接口体内
类: 类体内
指定泛型对应的具体类型: <具体类型名>
函数: 调用函数时函数名的右侧
接口: 定义实现类时接口名的右侧
类: 创建实例时类名的右侧
Vue3
1. 聊聊Vue3
2020 年 9 月份发布的正式版
Vue3支持vue2的大多数特性
设计了一套强大的组合API来替代option API: 可读性和可复用性更好
更好的支持Typescript
使用Proxy代替defineProperty实现数据响应式: 效率更高, 功能更强大
重写虚拟DOM的实现和Tree-Shaking: 更小更快
设计了一个新的脚手架工具vite: 开发启动快了很多
2. 比较Vue2与Vue3的响应式
1) vue2的响应式
核心:
对象: 通过defineProperty对对象的已有属性值的读取和修改进行劫持(监视/拦截)
数组: 通过重写数组更新数组一系列更新元素的方法来实现元素修改的劫持
调用数组原生的对应, 对数组元素进行操作
更新界面
arr[3].name = 'abc' // 修改的是元素对象内部的属性
arr[3] = {name: 'abc'} // 这个操作不会导致界面更新
问题
对象直接新添加的属性或删除已有属性, 界面不会自动更新 (defineProperty)
直接通过下标替换元素或更新length, 界面不会自动更新 arr[1] = {}
2) Vue3的响应式
核心:
通过Proxy(代理): 拦截对data任意属性的任意(13种)操作, 常用 3 个: 读取属性值, 修改或
添加属性, 删除属性
通过 Reflect(反射): 动态对被代理对象的相应属性进行特定的操作
解决vue2下面 3 个不更新界面的问题
给对象添加新属性
删除对象已有属性
直接通过下标替换元素或更新length
3. 常用的Composition API
setup: 所有组合API都在此选项函数中执行(它本身并不是组合API)
ref : 一般用来定义基本类型数据的响应式
reactive: 一般用来定义包含多个数据的对象/数组的响应式
computed: 定义基于已有响应式数据的计算属性(getter/setter)
watch: 监视响应式数据
toRefs: 将一个被代理对象中所有属性都转换为ref对象
onMounted: 当初始化挂载显示后执行回调
七. React
简单版
2 种组件
state的 3 个操作
函数组件: 不能有state
类组件: 可以有state
- 初始化状态数据 this.state =
- 读取状态数据: this.state.xxx
- 更新状态数据: this.setState({xxx: 4})
类组件的生命周期
组件间通信:
2 种路由跳转方式:
向路由组件传递数据的方式
详解深入版
1. react 生命周期函数
最新的生命周期:
初始化
constructor (初始化状态 - ref - 绑定 this 指向)
static getDerivedStateFromProps / componentWillMount(旧)
render(返回渲染页面需要的虚拟 dom 对象)
componentDidMount(发送 ajax 请求、设置定时器等一次性任务)
更新
static getDerivedStateFromProps / componentWillReceiveProps(旧)
shouldComponentUpdate(性能优化,减少重新渲染次数)
componentWillUpdate(旧)
render
getSnapshotBeforeUpdate
componentDidUpdate(更新时发请求)
卸载
componentWillUnmount(收尾工作,如清除定时器、取消 ajax 请求)
即将废弃
componentWillMount
componentWillUpdate
componentWillReceiveProps
初始化(挂载): componentWillMount => render => componentDidMount
更新: componentWillUpdate => render => componentDidUpdate
卸载: componentWillUnmount
props: 实现父子组件间通信
函数: 子向父通信
非函数: 父向子通信
pub-sub/redux: 实现任意组件间通信
1) 声明式: <NavLink> / <Link>
2) 编程式: props.history.push/replace()
1). params参数
2). query参数(有点麻烦, 需要进一解析才可以用)
3). state (1. 必须history模式, 2. 编程式路由导航) push(path, state数据)
处理异常
static getDerivedStateFromError(error)
通过更新状态 --> 使下一次渲染可以显降级 UI
componentDidCatch()
记录错误信息
2. react 性能优化?
shouldComponentUpdate
通过对比新旧 state 和 props 来决定是否重新渲染
PureComponent
实现了新旧 state 和 props 的浅比较
为了防止修改原对象,从而让浅比较比较不出来(确保前后数据一定不一样),可以使用
immuable.js
3. 为什么虚拟 dom 会提高性能?
虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要
的 dom 操作,从而提高性能。
具体实现步骤如下:
用 JavaScript 对象结构表示 DOM 树的结构
然后用这个树构建一个真正的 DOM 树,插到文档当中当状态变更的时候,重新构造一
棵新的对象树。
然后用新的树和旧的树进行比较,记录两棵树差异把 2 所记录的差异应用到步骤 1 所构
建的真正的 DOM 树上,视图就更新了。
4. 虚拟 DOM diff 算法
虚拟 DOM diff 算法主要就是对以下三种场景进行优化:
tree diff
对树进行分层比较,两棵树只会对同一层次的节点进行比较。(因为 DOM 节点跨层级的
移动操作少到可以忽略不计)
如果父节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比
较。
注意:
React 官方建议不要进行 DOM 节点跨层级的操作,非常影响 React 性能。
在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS
隐藏或显示节点,而不是真的移除或添加 DOM 节点。
component diff
如果是同一类型的组件,按照原策略继续比较 virtual DOM tree(tree diff)。
对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道
这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过
shouldComponentUpdate() 来判断该组件是否需要进行 diff。
如果不是,直接替换整个组件下的所有子节点。
element diff
对处于同一层级的节点进行对比。
这时 React 建议:添加唯一 key 进行区分。虽然只是小小的改动,性能上却发生了翻天
覆地的变化!
如: A B C D --> B A D C
添加 key 之前: 发现 B != A,则创建并插入 B 至新集合,删除老集合 A;以此类
推,创建并插入 A、D 和 C,删除 B、C 和 D。
添加 key 之后: B、D 不做任何操作,A、C 进行移动操作,即可。
建议:在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数
量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
总结
React 通过制定大胆的 diff 策略,将 O(n3) 复杂度的问题转换成 O(n) 复杂度的问题;
React 通过分层求异的策略,对 tree diff 进行算法优化;
React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component
diff 进行算法优化;
React 通过设置唯一 key 的策略,对 element diff 进行算法优化;
建议,在开发组件时,保持稳定的 DOM 结构会有助于性能的提升;
建议,在开发过程中,尽量减少类似将最后一个节点移动到列表首部的操作,当节点数
量过大或更新操作过于频繁时,在一定程度上会影响 React 的渲染性能。
5. setState 如何使用?
this.setState(obj) 更新,多次更新会合并,最后一次生效
this.setState(obj, callback) callback 会在界面更新后调用
this.setState(changeFn, callback) changeFn更新,多次更新会依次执行
6. 调用 setState 真的是异步吗?
setState 只在 React 合成事件和钩子函数中是“异步”的,在原生 DOM 事件和定时器中都是
同步的。
如果需要获取“异步”场景的 setState 的值 --> this.setState(partial, callback) 在
callback 中拿到最新的值
如果要在“异步”场景保证同步更新多次 setState --> this.setState((prevState,
props) => {return newState})
7. React 中 key 的作用是什么?
虚拟 DOM 的 key 的作用?
简单说: key 是虚拟 DOM 对象的标识, 在更新显示时 key 起着极其重要的作用
详细说: 当列表数组中的数据发生变化生成新的虚拟 DOM 后, React 进行新旧虚拟 DOM
的 diff 比较
key 没有变
item 数据没变, 直接使用原来的真实 DOM
item 数据变了, 对原来的真实 DOM 进行数据更新
key 变了
销毁原来的真实 DOM, 根据 item 数据创建新的真实 DOM 显示(即使 item 数
据没有变)
key 为 index 的问题
添加/删除/排序 => 产生没有必要的真实 DOM 更新 ==> 界面效果没问题, 但效率低
如果 item 界面还有输入框 => 产生错误的真实 DOM 更新 ==> 界面有问题
注意: 如果不存在添加/删除/排序操作, 用 index 没有问题
解决:
使用 item 数据的标识数据作为 key, 比如 id 属性值
8. 何为受控组件(controlled component)
能在表单项输入过程中进行实时数据收集的表单组件
原理: 绑定onChange事件监听, 当输入改变时, 将最新的值, 更新到对应的state上
9. 何为高阶组件(higher order component)
概念:高阶组件就是一个函数,且该函数接受一个组件作为参数,并返回一个新的组件
目的: 能让其他组件复用相同的逻辑
当你发现两个组件有重复逻辑时,就使用 HOC 来解决。
用法详见:https://juejin.im/post/5c972f985188252d7f2a3eb0
10. 组件间通信有哪些方式
props
子 --> 父,父组件传函数数据给子组件,子组件调用修改父组件数据
父 --> 子,父组件传非函数数据给子组件
兄弟(一般不用),数据定义在公共的父组件中。
context
适用于祖孙组件
const context = React.createContext()
context.Provider / context.Consumer
pubsub-js
适用于兄弟组件、祖孙组件。
pubsub.subscribe('MSG', (msg, data) => {})
pubsub.publish('MSG', data)
注意:
先订阅再发布
订阅只能一次,发布可以多次
redux
管理多个组件共享的数据
web storage ==> localStorage/sessionStorage
保存在浏览器本地,引入使用
也可以跨页面通信
11. 谈谈 redux
作用: 集中管理多个组件共享的状态
特点: 单一数据源、纯函数、只读 state
核心:
store
用来集中存储数据的
action-creators ===> actions
用来生成 action 对象 {type: xxx, data: xxx}
分为:同步和异步,同步返回值是对象,异步返回值是函数
action-types
用来定义 action 对象的 type
reducers
根据之前的状态和 action 对象来生成新状态,并更新 store 对象的数据
一种状态数据对应一个 reducer 函数
工作流程
组件调用 action-creators 生成 action 对象
组件调用 store 对象的 dispatch 方法,分发 action 对象
此时会自动触发 reducer 函数调用(遍历所有 reducer 函数直到匹配上)
reducer 函数一旦调用就会返回一个新的状态
新状态会交给 store 对象管理,从而更新状态
一旦状态更新,就会触发 store.subscribe 订阅的函数,从而重新渲染组件
组件重新渲染就能获取最新的 store 对象的值了
扩展概念:
UI 组件:负责界面展示,没有 redux 内容
容器组件:负责数据操作,仅有 redux 内容,将其传给 UI 组件
12. 前端路由实现原理
Hash 模式
window 对象提供了 onhashchange 事件来监听 hash 值的改变,一旦 url 中的 hash 值
发生改变,便会触发该事件。
History 模式
使用 pushState 方法实现添加功能
使用 replaceState 实现替换功能
监听历史栈信息变化,变化时重新渲染
13. react 路由组件懒加载
Suspense
lazy()
() => import('./About.jsx')
14. React 和 vue 的区别
相同点:
都是组件化编程, 由数据驱动视图
在内部都使用vDOM与DOM Diff来提高性能
本身都主要关注于UI, 如路由/状态管理/ajax请求都是由其它的插件来完成
不同点:
React: 动态界面是JSX, 只有单向数据绑定, 不能直接更新状态数据, 而是指定新的状态数
据
Vue: 动态界面是template, 有双向数据绑定, 可以直接更新状态数据
15. React Hooks
让函数组件拥有状态数据和生命周期函数
React.useState
React.useEffect(callback, [])
React.useRef()
React.useContext()
八. Webpack
1. Webpack 基本概念
1) entry 入口
以某个文件为入口开始打包
分类
单入口 String
只会输出一个文件
多入口 Array / Object
Array 只会输出一个文件
Object 会输出多个文件 ==> 多页应用(MPA)
2) output 输出
打包后资源输出到哪里去
输出的文件名叫什么
3) loader 加载器
webpack 本身只能识别 json、js 模块,其他模块一旦加载就会报错
需要借助 loader 帮助 webpack 识别其它识别不了的模块
4) plugins 插件
loader 功能有限,要想做功能更加强大的工作交给插件
比如在页面中自动引入打包生成的js/css, 压缩css, 拷贝文件等
5) mode
模式:开发环境(development)和生产环境(production)
提供一系列默认配置, 用于简化配置
2. Webpack 基本配置
1) 处理JS文件
eslint-loader
在package.json中配置eslintConfig来指示eslint-loader到底要干什么事
enfore: 'pre' 优先执行
babel-loader
在webpack配置中配置babel来指示babel-loader到底要干什么事
babel.config.js: 配置webpack的preset与plugin
2) 处理Vue文件
vue-loader
3) 处理JSX文件
babel-loader
4) 处理CSS文件
开发环境:创建style标签插入样式
style-loader
css-loader
postcss-loader
less-loader / sass-loader / stylus-loader
生产环境:提取单独css文件,将来通过link引入
MiniCssExtractPlugin.loader(还需要配置插件 new MiniCssExtractPlugin)
css-loader
postcss-loader
less-loader / sass-loader / stylus-loader
5) 处理HTML文件
目标: 自动引入打包生成的js/css
html-webpack-plugin
6. 处理图片/字体/音视频文件
url-loader / file-loader
limit: 10000 小于10kb一下的图片会被base64处理
3. Webpack 优化手段
1) 优化打包构建速度
HMR 热模块替换
为什么要用?
默认情况下,一旦修改了代码,全部代码重新编译刷新,速度慢(全体刷新)
有什么作用?
只更新修改的模块,其他模块不变(局部更新)
怎么使用?
devServer: { hot: true }
new webpack.HotModuleReplacementPlugin()
注意:
默认情况下只有样式文件有HMR功能(style-loader),JS是没有的
开启JS的HMR功能:
手写JS代码 --> module.hot.accpet('模块路径', () => {})
在Vue使用 --> vue-loader
在React使用 --> react-hot-loader
缓存
eslint和babel两个任务处理JS文件,时间一般会比较长,为了让其重新构建速度更快, 可以使
用缓存。
eslint --> cache: true
babel --> cacheDirectory: true
cache-loader放置在要缓存loader的前面
注意:一般只针对耗时长的任务:eslint-loader/babel-loader/vue-loader
oneOf
作用:
让模块只被一个loader处理,其他的就不看了(原本所有都会判断一下)
能够提升打包速度
注意:
eslint-loader: 处理js, 需要先执行, 将其定义在oneOf的外面
babel-loader: 处理js, 后执行, 将其定义在oneOf的内部
多进程打包
过去: happyPack
现在: thread-loader
用法和cache-loader差不多,放在要使用loader前面
作用:开启多进程处理前面的任务,提升打包速度
注意:每个进程开启和通信都有开销,一般只针对耗时长的任务:babel-loader
2) 优化打包代码体积和性能
兼容性处理
JS
ES6由二个部分
新语法: const/let/箭头函数/解构赋值/对象简写
新API: Promise / 数组新方法/ 对象新方法
babel-loader presets: ['@babel/preset-env'] 问题就是只能编译语法, 不能处理新API
@babel/polyfill 做API兼容,问题是体积太大了
core-js3 在@babel/preset-env基础上,增加了useBuiltIns: 'usage'来实现按需加载
指定浏览器版本或占有率配置, 进一步减小打包文件: 只打包使用了且浏览器没有实现的
CSS
postcss-loader / autoPrefixer
在package.json中指定browserslist来指示postcss-loader兼容性做到什么程度
tree shaking( 摇树)
去除没有使用的JS代码
必须使用ES6模块化(需要禁止@babel/preset-env转换ES6模块化语法 modules: false)
开启webpack的生产模式(内部启用TerserPlugin,用来压缩JS代码的插件,tree shaking功
能就是这个插件完成的)
在package.json配置sideEffects来指定哪些文件需要进行tree shaking
code split 代码分割 / lazy loading 懒加载
作用:
抽取公共代码
拆分多个文件,减少单个文件体积(避免单次请求时间过长)
配置:
多入口 + optimization
将node_modules抽取成单独模块
将多入口的公共模块也抽取成单独模块
单入口 + optimization + import
将node_modules抽取成单独模块
动态导入语法import就能将某些文件抽取成单独模块
import()动态引入模块
原生 JS: 在需要的回调函数中动态加载模块, import(模块).then()
Vue: () => import('./Foo.vue'), 实现路由组件懒加载
React: Suspence +lazy(() => import('./SomeComponent')), 实现路由组件懒加载
preload 和 prefetch 预加载
作用:
让资源提前加载
区别:
preload 让当前页面的要使用资源加载(延后加载)
prefetch 让后面要使用资源提前加载(当前不需要使用)
使用:
import(/*webpackChunkName: xxx webpackPrefetch: true */'./xxx')
import(/*webpackChunkName: xxx webpackPreload: true */'./xxx') // 没有效果
问题:兼容性较差
使用Chrome团队提供的一个工具包: preload-webpack-plugin
npm i -D preload-webpack-plugin@next // 必须是最新的下一个版本
对异步模块包使用: prefetch
对同步模块包使用: preload
浏览器cache/缓存
hash
webpack每次构建都会生成一个新的且唯一的hash
问题:只要webpack重新构建,所有文件的hash都会发生变化,缓存就会失效
chunkhash
打包属于同一个chunk,就共享同一个hash
问题:样式文件被css-loader打包js文件中,导致样式文件和js文件属于同一个chunk,
共享同一个hash
一旦样式文件发生变化,js文件也会变
contenthash
根据文件的内容来成hash,所以只要文件内容不一样,hash就不一样
问题:
比如A模块有一个依赖,是B模块,那么A模块内部就会保存B模块hash值,
一旦B模块发生变化,B模块的hash值就会变,导致A模块内部保存B模块hash值也
发生改变
此时A模块文件内容发生变化,它的hash值也会变
解决:
runtimechunk: true 将A模块保存的B模块hash值存到runtime文件中,这样A模块
内部就没有B模块的hash值了,就不会因为B模块的修改而修改
九. 使用nginx部署项目
打包项目
前台PC: yarn build ==> 代理路径为/api ==> 后台接口也有/api
后台Admin: yarn build:prod ==> 代理路径为/prod-api ==> 后台接口没有/prod-api
注意: 打包后的项目一定要放在一个没有中文和空格的路径下才能部署运行
理解nginx
作为服务器运行前端项目(gshop-client / gshop-admin)
作为代理服务器, 转发前端项目的请求到后台接口
配置nginx
config/nginx.conf
# 部署前台项目
server {
listen 8081;
server_name localhost;
# 处理所有与后面不匹配的请求
location / {
root D:\work\online\gshop-client; # 项目资源的根目录
index index.html index.htm; # 首页页面
# try_files $uri $uri/ /index.html; # 所有 404 的请求返回index页面
}
# 处理所有/api开头的请求
# 转发到后台接口地址
# 转发时不要去掉/api
location /api {
proxy_pass http://39.98.123.211;
}
}
# 部署后台项目
server {
listen 8082;
server_name localhost;
location / {
root D:\work\online\gshop-admin;
index index.html index.htm;
}
解决history模式, 浏览器刷新 404 问题
根据vue-router的官方文档修改配置
区别正向代理与反向代理
正向代理
代理客户端(浏览器)去请求客户端无法访问目标服务器资源, 目标服务器是不知道客户端
的存在的
常见的: 开发环境配置的代理 / 翻墙工具
反向代理
代理对目标服务器来受理客户端的请求, 并转发给目标服务器处理, 客户端不用知道目标
服务器的存在
常见的: 生产环境打包后的nginx / 百度用来做负载均衡的nginx
十. HTML + CSS
标签语义化的意义
# 所有/api开头的请求都转发到后台接口地址
# 转发时去掉/prod-api
location /prod-api/ {
proxy_pass http://39.98.123.211/;
}
}
location / {
...
# 解决history模式, 浏览器刷新 404 问题
try_files $uri $uri/ /index.html; # 所有 404 的请求返回index页面
}
1. 开发者更容易理解,减少差异化,方便团队开发和维护
2. 机器更容易理解结果(搜索爬虫、方便其他设备解析(读屏幕软件、盲人设备、移动设备)
写页面结构应该注意的
1. 尽可能少的使用没有语义的 div 和 span 元素
2. 块级元素和内联元素的嵌套一定要符合 web 标准,比如内联元素就是不能嵌套块级元素
HTML5 新特性
1. 新的语义化元素:article 、footer 、header 、nav 、section
2. 表单增强,新的表单控件:calendar 、date 、time 、email 、url 、search
3. 新的 API:音频(用于媒介回放的 video 和 audio 元素)、图形(绘图 canvas 元素)
4. 新的 API:离线,通过创建 cache manifest 文件,创建应用程序缓存
5. 新的 API:本地存储,localStorage-没有时间限制的数据存储,sessionStorage-session 数据
存储(关闭浏览器窗口数据删除)
6. 新的 API:实时通讯,设备能力
CSS3 新特性
1. CSS3实现圆角(border-radius),阴影(box-shadow),
2. 对文字加特效(text-shadow、),线性渐变(gradient),旋转(transform)
3. transform:rotate(9deg) scale(0.85,0.90) translate(0px,-30px) skew(-9deg,0deg);//旋转,缩
放,定位,倾斜3.
4. 增加了更多的CSS选择器 多背景 rgba
5. 在CSS3中唯一引入的伪元素是::selection.
6. 媒体查询,多栏布局
盒模型
页面渲染时,dom 元素所采用的 布局模型 。可通过box-sizing进行设置。根据计算宽高的区域可
分为:
content-box (W3C 标准盒模型)
border-box (IE 盒模型)
padding-box
margin-box (浏览器未实现)
BFC
块级格式化上下文 ,是一个独立的渲染区域,让处于 BFC 内部的元素与外部的元素相互隔离,使 内外元素的定位不会相互影响。
IE 下为 Layout,可通过 zoom:1 触发
触发条件:
根元素
position: absolute/fixed
display: inline-block / table
float 元素
ovevflow !== visible
规则:
属于同一个 BFC 的两个相邻 Box 垂直排列
属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠
BFC 中子元素的 margin box 的左边, 与包含块 (BFC) border box 的左边相接触 (子元
素 absolute 除外)
BFC 的区域不会与 float 的元素区域重叠
计算 BFC 的高度时,浮动子元素也参与计算 - 文字层不会被浮动层覆盖,环绕于周围
应用:
阻止margin重叠
可以包含浮动元素 —— 清除内部浮动(清除浮动的原理是两个div都位于同一个 BFC 区
域之中)
自适应两栏布局
可以阻止元素被浮动元素覆盖
选择器优先级
!important > 行内样式 > #id > .class > tag > * > 继承 > 默认
选择器 从右往左 解析
去除浮动影响,防止父级高度塌陷
通过增加尾元素清除浮动
:after / <br> : clear: both
创建父级 BFC
父级设置高度
link 与 @import 的区别
link功能较多,可以定义 RSS,定义 Rel 等作用,而@import只能用于加载 css
当解析到link时,页面会同步加载所引的 css,而@import所引用的 css 会等到页面加载完
才被加载
@import需要 IE5 以上才能使用
link可以使用 js 动态引入,@import不行
CSS 预处理器(Sass/Less/Postcss)
CSS 预处理器的原理: 是将类 CSS 语言通过 Webpack 编译 转成浏览器可读的真正 CSS。在这层编 译之上,便可以赋予 CSS 更多更强大的功能,常用功能:
嵌套
变量
循环语句
条件语句
自动前缀
单位转换
mixin 复用
单行文本溢出
多行文本溢出
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex布局
flex:1,单独给某一个子元素设置flex属性的时候可以提高当前flex的权重
圣杯布局
圣杯特点简单记为:有头、有尾、包三列,圣杯布局中间有container大容器包裹着左、中、
右三列区域
左、中、右是独立的三个区域,都处于一个层级
双飞翼布局
左、中、右是独立的三个区域,中间区域属于最上面的层级
十一. 移动端
基础知识
1 英寸(inch) = 2.54厘米(cm)
IPhone 6 的屏幕分辨率为 750 * 1334 设备独立像素为 375 * 667
物理像素:由屏幕制造商决定,屏幕生产后无法修改
css像素:单位是px,它是为 Web 开发者创造的
设备独立像素的出现,使得即使在【 高清屏 】下,也可以让元素有正常的尺寸,让代码不受到
设备的影响,它是设备厂商根据屏幕特性设置的,无法更改。
1 个位图像素对应 1 个物理像素,图片才能得到完美清晰的展示
pc端视口:默认宽度和浏览器窗口的宽度一致,也被称为初始包含块
document.documentElement.clientWidth
移动端视口:
布局视口:一般是 980px 左右,布局视口经过压缩后,横向的宽度用css像素表达就不再
是375px了,而是980px
视觉视口:用户可见的区域,它的绝对宽度永远和设备屏幕一样宽
理想视口:布局视口宽度 与 屏幕等宽(设备独立像素),靠meta标签实现
适配
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2 ; /*2*/
-webkit-box-orient: vertical;
<meta name="viewport" content="width=device-width,initial-scale=1.0"
/>
1.viewport 适配
方法:拿到设计稿之后,设置布局视口宽度为设计稿宽度,然后直接按照设计稿给宽高进行布
局即可。
一般适用于:计图稿宽度 < 375
2.rem适配
方案一:(百度)
1. 设置完美视口
2. 通过js设置根字体大小 = ( 当前设备横向独立像素值 *100) / 设计稿宽度
3. 编写样式时,直接以rem为单位,值为: 设计值 / 100
4. 增加 JS 代码进行实时适配
方法二:(淘宝、搜狐、唯品会)
1. 设置完美视口
2. 通过js设置根字体大小 = 当前设备横向独立像素值 / 10
3. 编写样式时,直接以rem为单位,值为: 设计值 / (设计稿宽度 / 10)
4. 增加 JS 代码进行实时适配
3.vw适配
vw和vh是两个相对单位
1vw = 等于布局视口宽度的1%
1vh = 等于布局视口高度的1%
4.1px物理像素边框
高清屏幕下 1px 对应更多的物理像素,所以 1 像素边框看起来比较粗,解决方法如下
方法一
使用媒查询:
方法二
根据dpr扩大布局视口,例如dpr为n则布局视口改为原来的n倍,则元素尺寸均变为原来的n分之 一,为了保证元素尺寸比例不变,扩大根字体为原来的n倍,但整个过程中边框一直用px作为单 位,不用rem。
1. rem 页面布局
2. 元素的边框设置为 1px
3. 通过 viewport 中的 initial-scale 将布局视口扩大n倍,这样页面元素就比原来缩小了n倍
@media screen and (-webkit-min-device-pixel-ratio: 2 ){
#demo{
border: 0.5px solid black;
}
}
或
@media screen and (-webkit-min-device-pixel-ratio: 2 ){
#demo2::after{
transform:scaleY(0.5);
}
}
4. 重新设置根元素字体
移动端事件
touchstart 元素上触摸开始时触发
touchmove 元素上触摸移动时触发
touchend 手指从元素上离开时触发
touchcancel 触摸被打断时触发
移动端中touchstart,touchend,click执行顺序
touchstart
touchend
click,浏览器在 click 后会等待约300ms去判断用户是否有双击行为,如果300ms内没有再一
次click,那么就判定这是一次单击行为
点击穿透
touch 事件结束后会默认触发元素的 click 事件
方法一:阻止默认行为
方法二:使背后元素不具备click特性,用touchXxxx代替click
方法三:让背后的元素暂时失去click事件, 300 毫秒左右再复原,属性pointer-events:
none;
方法四:让隐藏的元素延迟 300 毫秒左右再隐藏
getComputedStyle
可读取到非内联样式上的属性
可以通过使用 getComputedStyle 读取样式,通过 element.style 修改样式
十二. 小程序
1) 小程序特点
1. 体积小,压缩包的体积不能大于2M
2. 没有DOM对象
3. 基于组件化开发
4. 小程序环境不同于浏览器环境
1. 浏览器环境中全局对象: window
2. 小程序环境中全局对象:wx
3. 小程序环境中没有window对象
var viewport = document.querySelector('meta[name=viewport]')
var scale = 1 / window.devicePixelRatio
viewport.setAttribute('content', 'width=device-width,initial-scale=' +
scale);
var fontSize = parseInt(document.documentElement.style.fontSize);
document.documentElement.style.fontSize = fontSize * window.devicePixelRatio
+ 'px'
2)小程序适配
1. 小程序适配单位: rpx(responsive px) 响应式单位
2. 小程序中规定所有的机型中页面宽度都是750rpx
3. iphone6: 1物理像素 = 0.5px = 1rpx ---> 1px = 2rpx
4. 小程序底层已经做了viewport适配
3) 小程序相关语法
1. 数据绑定
1. 单项数据流: Model ---> view
2. 修改状态数据: this.setData() 同步行为
3. 页面中的数据均来自于data中,使用data中的数据的时候记住使用表达式{{}}
2. 事件绑定
1. 事件分类: 冒泡事件 || 非冒泡事件
2. 绑定的方式: bind + 事件名 || catch + 事件名
3. 路由跳转
1. wx.navigateTo() || wx.redirectTo()
4. 本地存储
1. 语法:
1. wx.setStorage() || wx.setStorageSync()
2. wx.getStorage() || wx.getStorageSync()
2. 注意点
1. 单个key上限是1M
2. 整体上限是10M
5. 全局传参
app.js中可定义全局参数globalData
4) 向事件对象传参
1. 语法
1. id=value
2. data-key=value
2. 获取
1. id: event.target.id || event.currentTarget.id
2. data-key: event.target.dataset.key || event.currentTarget.dataset.key
3. 使用场景
1. id:通常需要向event对象传递单一且唯一的标识
2. data-key: 需要向event对象传递多个标识数据
5) 小程序获取用户唯一标识(openId)
1. 客户端先通过wx.login(获取code
2. wx.request()发送code给服务器
3. 服务器端发送请求携带参数(code, appSecret(开发密钥), appId)给微信服务器获取openId
4. 微信返回openId给服务器后,服务器进行加密再返回给前端
5. 前端进行本地储存,下次发送请求时携带参数
6) 小程序支付流程
1. 用户在小程序客服端下单(包含用户及商品信息)
2. 小程序客户端发送下单支付请求给商家服务器
3. 商家服务器同微信服务器对接获取唯一标识openID
4. 商家服务器根据openId生成商户订单(包含商户信息)
5. 商家服务器发送请求调用统一下单API获取预支付订单信息
6. 商家对预支付信息签名加密后返回给小程序客户端
7. 用户确认支付(鉴权调起支付)
8. 微信服务器返回支付结果给小程序客户端
9. 微信服务器推送支付结果给商家服务器端
7) 面试问题
Iphone6的dpr为多少?Iphone6Plus比Iphone6显示图像清晰吗?
视网膜屏幕是分辨率超过人眼识别极限的高分辨率屏幕,Iphone4的dpr = 2; 人类肉眼分辨的
极限
生命周期函数实际测试和官网描述有差
wx.setStorage(),单个 key 允许存储的最大数据长度为 1MB,所有数据存储上限为 10MB
性能优化
一个页面一次只有一个video标签,其余的使用image代替
请求相关
小程序为了安全起见只支持Https请求
wx.request最大并发限制 10 个
如何使用组件
先创建组件,在对应的json文件下设置component: true
到使用组件页面的json文件中,注册组件填写相应的路径
使用npm包
下载后,勾选使用npm模块,再构建npm,会将node_modules中的包打包到
miniprogram_npm中
分包相关
分包后可解决2M限制,并且能分包加载内容,提高性能
分包后单个包的体积不能大于2M,所有包的体积不能大于16M
1. 常规分包
加载小程序的时候先加载主包,当需要访问分包的页面时候才加载分包内容
分包的页面可以访问主包的文件,数据,图片等资源
2. 独立分包
独立分包可单独访问分包的内容,不需要下载主包
独立分包不能依赖主包或者其他包的内容
通常某些页面和当前小程序的其他页面关联不大的时候可进行独立分包,如:临时加的
广告页 || 活动页
3. 分包预下载
在加载当前包的时候可以设置预下载其他的包
缩短用户等待时间,提高用户体验
8) 开发相关
setData行为始终是同步的
H5中实现滑块功能,推荐大家使用:better-scroll
9) 封装
封装函数:
1. 功能点明确
2. 函数内部保留静态的数据
3. 将动态的数据抽取出来作为形参,由使用者根据场景不同动态传入实参
封装组件:
1. 功能点明确 || 呈现效果
2. UI组件 || js组件
3. 组件内部保留静态的数据
4. 将动态的数据抽取出来作为 props属性,由使用者根据场景不同动态以标签属性的形式
导入
5. 封装良好的组件应该规定传入props数据的必要性还有数据类型 propsType