Vue总结

1.组件生命周期

1.1 单组件生命周期

​ 单组件的生命周期为beforeCreate–created–beforeMount–mounted;

1.2 父子组件生命周期

​ 父子组件的生命周期为beforeCreate–created–beforeMount–(Child)beforeCreate–(Child)created–(Child)beforeMount–(Child)mounted–mounted;

​ 父组件在创建完成后,挂在完成之前开始进行子组件的创建和挂载;

1.3 缓存组件生命周期

​ 若组件为缓存组件(外层包有keep-alive),则会有activated生命周期;此时生命周期执行顺序为:

……(同父子组件)–(Child)activated–activated;

​ 当销毁组件时,会执行 : beforeDestroy – (Child)beforeDestroy–(Child)destroyed–destroyed;

​ 当两个缓存组件相互切换时,会优先执行当前组件的deactivated,然后执行被挂载组件的activated;

1.4 捕获错误生命周期

1
2
3
4
5
6
7
// 捕获子组件传递过来错误
errorCaptured (err, child, info) {
console.log('errorCaptured', err, child, info)
alert(err.message)

return false // 不再向外传递, 说明当前已经处理了错误
},

2.组件详解

2.1 动态组件

​ 动态组件就是可以通过is来进行组件之间的切换(is绑定动态数据), is 后面的名字可以为已经注册过的组件的名字;或者一个组件的选项对象;当多个组件之间来回切换时,其他未被选中的组件将会失活.

2.2 异步组件

​ 异步组件就是定义的时候什么都不做,只在组件需要渲染(组件第一次显示)的时候进行加载渲染并缓存,缓存是以备下次访问。比如对路由的懒加载.

​ import()(此处的import是方法,用于动态引入组件)的执行会使被引入的模块单独打包

​ 懒加载:

1
2
路由组件的配置是一个函数: 函数开始不执行 ==> 默认不加载组件的打包文件 
==>第一次访问组件的路由路径时才请求加载打包文件 ==> 这就是懒加载的效果

​ 注: import动态引入(懒加载)中import方法的返回值是一个promise对象

2.3 缓存组件

​ 缓存组件指的是被keep-alive包裹的组件,keep-alive有两个属性,分别是:

1
2
include: 指定只缓存哪些名称的组件(使用时只会缓存出现在include内的组件,存放多个组件时需要写成组件的数组)
exclude: 不绑在哪些名称的组件(用法同上,使用时不会缓存写入的组件)

2.4 函数式组件

1
2
3
不实例化, 内部没有任何生命周期处理函数
无状态, 但可以接收props
轻量,渲染性能高,适合只依赖于外部数据传递而变化的组件(展示组件,无逻辑和状态修改)

​ 函数式组件需要设置functional : true,意为当前组件为函数式组件;渲染页面需要使用render方法,使用方式类似react,需要以jsx的方式创建虚拟对象;

1
2
3
4
5
6
7
8
9
10
11
12
render (createElement, context) {
// 不能通过this取props, 当前是函数组件, 没有组件实例
const {title, imgUrl} = context.props // 包含组件标签传入的所有标签属性
const vNodes = []
if (title) {
vNodes.push(<h2>{title}</h2>) // jsx ==> 创建虚拟DOM对象
}
if (imgUrl) {
vNodes.push(<img src={imgUrl}/>)
}
return vNodes // 返回的是包含多个虚拟DOM的数组
}

​ 当前组件最终渲染一个什么界面,由当前函数的返回的虚拟DOM决定

2.5 递归组件

​ 递归组件顾名思义为自己调用自己的组件,即在组件的内部有自己的组件标签;常用语结构相似的多重组件内,例如element-ui中的树形控件tree;

注 : 递归组件使用时必须有自己的名字name;

3.组件间通信(11种)

通信方式列表:

1
2
3
4
5
6
7
8
9
10
11
1) props
2) vue自定义事件
3) 全局事件总线 / pubsub(在vue开发中基本不用)
4) v-model
5) .sync
6) $attrs与$listeners 与哪2个语法配合使用? v-bind / v-on
7) $refs & $children & $parent
8) provide与inject
9) Vuex
10) 插槽 ==> 作用域插槽
11) pubsub(项目中很少使用)

组件间关系

1
2
3
4
父子
祖孙
兄弟
远亲(其它)

根据通信的2个组件间的关系来选择一种通信方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1) 父向子
props(非函数)
v-model
$refs, $children
插槽
2) 子向父
props(函数)
vue自定义事件
v-model
.sync
$parent
作用域插槽
3) 祖孙间
$attrs与$listeners 与v-bind/v-on配合使用
provide与inject
4) 兄弟或其它/任意
全局事件总线
Vuex

1. props

1
2
3
1) 实现父向子通信: 属性值是非函数
2) 实现子向父通信: 属性值是函数
3) 应用: 最基本, 用得最多的方式

2. vue自定义事件

1
2
3
4
5
6
7
8
9
10
1) 实现子组件向父组件通信
2) 相关语法:
父组件中绑定自定义事件监听:
<Child @eventName="callback($event)">
child.$on('eventName', callback)
子组件中分发事件
this.$emit('eventName', 2)
3) 应用:
elment-ui的组件的事件监听语法都用的是自定义事件 <el-button @click="test">
我们项目中的组件也用了不少自定义事件

自定义事件与原生事件的区别: 组件上的事件在不写.native的前提下都为自定义事件,此时通常为子组件分发事件,然后父组件中绑定自定义事件监听; 带上.native时将变为原生事件(要求事件名为原生事件的事件名);而原生标签上的事件为原生事件,是可以直接使用的.

3. 全局事件总线

1
2
3
4
5
6
7
8
9
10
1) 实现任意组件间通信
2) 编码:
将入口js中的vm作为全局事件总线对象:
beforeCreate() {
Vue.prototype.$bus = this
}
传递数据的组件分发事件: this.$bus.$emit('eventName', data)
接收数据的组件处理监听: this.$bus.$on('eventName', (data) => {})
3) 应用:
前台项目中使用全局事件总线

VueComponent.prototype = Object.create(Vue.prototype)

Object.create(Vue.prototype): 创建Vue原型对象的子对象,即新对象的原型对象为Vue.prototype; 最后组件实例对象vc的原型对象的原型对象就是Vue实例对象vm的原型对象.

4. v-model

1
2
3
4
5
6
7
8
9
10
11
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

5. .sync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

6. $attrs与$listeners

1
2
3
4
5
6
7
8
9
10
1) $attrs
实现当前组件的父组件向当前组件的子组件通信(祖孙间通信)
它是包含所有父组件传入的标签属性(排除props声明, class与style的属性)的对象
使用: 通过 v-bind="$attrs" 将父组件传入的n个属性数据传递给当前组件的子组件
2) $listeners
实现当前组件的子组件向当前组件的父组件通信 (孙向祖通信)
$listeners是包含所有父组件传入的自定义事件监听名与对应回调函数的对象
使用: 通过v-on="$listeners" 将父组件绑定给当前组件的事件监听绑定给当前组件的子组件
3) 应用
利用它封装了一个自定义的带hover文本提示的el-button

7. $refs & $children & $parent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1) $refs
实现父组件向指定子组件通信
$refs是包含所有有ref属性的标签对象或组件对象的容器对象
使用: 通过 this.$refs.child 得到子组件对象, 从而可以直接更新其数据或调用其方法更新数据
2) $children
实现父组件向多个子组件通信
$children是所有直接子组件对象的数组
使用: 通过this.$children 遍历子组件对象, 从而可以更新多个子组件的数据
3) $parent
实现子组件向父组件通信
$parent是当前组件的父组件对象
使用: 通过this.$parent 得到父组件对象, 从而可以更新父组件的数据
4) 应用
在后台管理项目中使用了$refs

8. provide与inject

1
2
3
4
5
6
7
8
9
10
11
12
13
1) 实现祖孙组件间直接通信
2) 使用
在祖组件中通过provide配置向后代组件提供数据
在后代组件中通过inject配置来声明接收数据
3) 注意:
不太建议在应用开发中使用, 一般用来封装vue插件
provide提供的数据本身不是响应式的 ==> 父组件更新了数据, 后代组件不会变化
provide提供的数据对象内部是响应式的 ==> 父组件更新了数据, 后代组件也会变化
方法二:
祖: 定义返回数据的方法, 通过provide提供这个方法
后代: 注入这个方法, 定义计算属性返回这个方法返回的数据
4) 应用:
element-ui中的Form组件中使用了provide和inject

​ provide提供的数据本身不是响应式的,希望是响应式,可以将数据放在data里的对象里,接收时使用inject(类似props)

9. Vuex

1
2
3
4
5
6
7
1) 实现任意组件间通信
2) Vuex 是一个专为 Vue 应用程序设计的管理多组件共享状态数据的 Vue 插件
任意组件都可以读取到Vuex中store的state/getters对象中的数据
任意组件都可以通过dispatch()或commit()来触发store去更新state中的数据
一旦state中的数据发生变化, 依赖于这些数据的组件就会自动更新
3) 应用
前台和后台项目都有用vuex管理组件数据

10. 插槽 ==> 作用域插槽slot-scope/v-slot

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1) 实现父组件向子组件传递标签内容
2) 什么情况下使用作用域插槽?
父组件需要向子组件传递标签结构内容
但决定父组件传递怎样标签结构的数据在子组件中
3) 编码:
子组件:
<slot :row="item" :$index="index"> <!-- slot的属性会自动传递给父组件 -->
</slot>
父组件:
<template slot-scope="{row, $index}">
<template v-slot="{row, $index}">
<span>{{$index+1}}</span> &nbsp;&nbsp;
<span :style="{color: $index%2===1 ? 'blue' : 'green'}" >{{row.text}} </span>
</template>
4) 应用
element-ui中绝大部分组件都用了插槽
element-ui中的 table-column 组件使用了作用域插槽

11. pubsub

类似全局事件总线,但项目中不常使用;

发布消息 : pubsub.publish

订阅消息 : pubsub.subscribe

12. 路由组件间通信方式?

1
2
3
4
5
query参数
params参数
props(需要配置, 而不是标签属性)
meta(也是配置)
vuex

4. watch/methods和computed的区别

例:实现名和姓两个文本框的内容改变,则全名的文本框的值也跟着改变

1.1 methods方法实现

1
2
3
getFullName(){
this.fullName = this.firstName + '_' + this.lastName
}

​ 使用methods实现时,每当触发重新渲染时,调用方法method将总会再次执行函数;

1.2 computed实现

1
2
3
fullName(){
return this.firstName + '_' + this.lastName
}

​ 计算属性computed是基于它们的响应式依赖进行缓存的,只在相关响应式依赖发生改变时(即firstName 或者 lastName 中的1到 2个值发生变化时)它们才会重新求值;

1.3 watch实现

1
2
3
4
5
6
7
8
watch(){
firstName(value){
this.fullName = value + '_' + this.lastName
},
lastName(value){
this.fullName = this.firstName + '_' + value
},
}

​ watch监听的是一个变量(或者一个常量)的变化,这个变量可能是一个单一的变化也可能是一个数组。computed可以监听很多个变量,但是这个变量一定是vue实例里面的。

小结:

​ computed与watch的区别计算属性必须同步返回计算结果, 而watch中可以在异步操作后更新数据显示; watch可以深度监视, 计算属性只是监视了使用到的数据;

选择:

如果是根据现在的数据同步计算就可以确定要显示的另一个数据 ==> computed

如果涉及到异步操作/深度监视 ==> watch

一旦一个数据变化, 我们需要做一系列操作 ===> watch

​ computed与method的区别计算属性有缓存, 多次读取显示只计算一次method, 多处显示计算多次

5. 响应式

1) 几个重要问题?

  • mvvm的理解, 与MVC的区别?

MVC

Model(模型)是应用程序中用于处理应用程序数据逻辑的部分。
  通常模型对象负责在数据库中存取数据。

View(视图)是应用程序中处理数据显示的部分。
  通常视图是依据模型数据创建的。

Controller(控制器)是应用程序中处理用户交互的部分。
  通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。

等同于MVVM中的ViewModel…..

最典型的MVC就是JSP + servlet + javabean的模式

MVVM

M: Model模型, 也就是包含数据的js对象(只包含属性, 不包含方法) data对象

V: View视图,动态显示模型对象中的数据显示界面 模板页面

VM: ViewModel视图模型, 本质是一个绑定器, 通过vm读取model中的数据显示到view上, 同时view输入数据改变, vm也可以将输入数据保存到model中 vue/组件的实例

图解

MVVM的优势: 不用亲自操作DOM, 数据是响应式的, 一旦数据变化, 界面自动更新

  • 组件的data为什么只能是函数不能是对象?

    data作为对象时,多个组件会共享同一个data;而写成函数的形式时,每个组件都会有其自己的data.(因为是调用data函数返回的对象)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <A/>
    <A/>
    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
    • 同一个组件的多个组件实例的data必须是不同的对象(内容初始数据可以相同)
    • 如果是data是对象, 组件的多个实例共用一个data对象
    • 如果是函数, 组件对象通过调用函数得到的一个新的data对象
  • 响应式数据与非响应式数据?

    • 响应式: data / props / computed/ vuex的state与getters
    • 非响应式: 仅仅存在于组件对象上的属性数据
      • 给组件对象添加一个新属性: this.xxx = value
      • 直接给一个响应式对象添加一个新属性: this.product.xxx = ‘abc’ ==> this.$set(this.product, ‘xxx’, 3)
  • 对象的响应式与数组的响应式有什么区别?

    • 对象: 通过Object.defineProperty()添加setter方法来监视属性数据的改变 + 订阅-发布
    • 数组: 重写更新数组元素的一系列方法 + 订阅-发布
      • 调用原生的对应对数组元素进行相应的操作
      • 更新界面去
    • 为什么vue在数组上使用重写方法的形式来实现响应式?
      • 占用空间小, 效率高 ===> 不用给每个元素都加setter
      • 数组元素更新我们一般是调用其方法, 对象属性的更新我们基本都是通过.属性的方式
      • 这种方式的限制? 不能通过下标直接更新元素/length

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中的代码, 会自动合并到当前组件的配置中

特性:

1.mixins中的生命周期会与引入mixins的组件的生命周期整合在一起调用

2.组件的data、methods、filters会覆盖mixins里的同名data、methods、filters。

3.不同mixin里的同名方法,按照引进的顺序,最后的覆盖前面的同名方法。

缺点:

1.变量来源不明确(隐式传入),不利于阅读,使代码变得难以维护。

2.多个mixins的生命周期会融合到一起运行,但是同名属性、同名方法无法融合,可能会导致冲突。

3.mixins和组件可能出现多对多的关系,复杂度较高(即一个组件可以引用多个mixins,一个mixins也可以被多个组件引用)

hook介绍

  • 使用Vue3的组合API封装的可复用的功能函数
  • 自定义hook的作用类似于vue2中的mixin技术
  • 自定义Hook的优势: 很清楚复用功能代码的来源, 更清楚易懂.

与vue3中hooks比较有什么差别?

hook解决了功能代码来源不明的问题(mixins中为隐式引入,一旦mixins引入的多了,变量和方法的来源很难知晓)

2) 自定义指令

1
2
3
4
5
6
Vue.directive('upper-text', (el, binding) => {
el.innerText = binding.value.toUpperCase()
})
<p v-upper-text="msg"></p>

msg: 'I Will Back'

3) 自定义过滤器

1
2
3
4
5
6
7
// 注册全局过滤器
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>

4) 自定义插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 对象插件
const myPlugin = {
// 必须有此方法
install (Vue) {
// 通过Vue来扩展新的功能语法, 如注册全局组件/指令/过滤器/...
}
}
// 函数插件
const myPlugin = (Vue) => {
// 通过Vue来扩展新的功能语法, 如注册全局组件/指令/过滤器/...
}

export default myPlugin

// 在入口JS中引入, 并声明使用来安装插件
import myPlugin from './vue-myPlugin'
Vue.use(myPlugin)

问题: Vue.use()内部做了什么?

  • 对象插件: 调用插件对象install方法(传入Vue)来安装插件(执行定义新语法的代码)
  • 函数插件: 直接将其作为install来调用(传入Vue)来安装插件(执行定义新语法的代码)

7.vue状态管理vuex

1) vuex的5大属性

  • state
  • mutations
  • actions
  • getters
  • modules

2) vuex多模块编程

  • vuex的多模块编程的必要性
    • vuex单模块问题:
      • 需要的管理状态数据比较多, 那对应的mutations/actions模块就会变得比较大
      • 如果添加新的数据管理, 需要修改现在文件(不断向其添加内容)
    • vuex多模块编程: 对各个功能模块的数据分别进行管理, 这样更加具有扩展性
  • 什么时候需要用vuex多模块编程? 需要vuex管理的数据比较多时使用
  • 多模块编程的总state结构:
1
2
3
4
5
6
7
8
9
{
home: {
categoryList: [],
xxx: {}
},
user: {
userInfo: {}
}
}

4) 问题1: vuex中的mutation可以执行异步操作吗?

  • 功能可以 ==> 异步更新数据后界面确实会自动更新
  • 问题 ==> vuex的调试工具监视不到mutation中的异步更新, 工具记录还是更新前的数据(不对)
  • 扩展: 工具如何记录数据变化? ==> 每次mutation函数执行完后, 立即记录当前的数据 ==> 在mutation中同步更新state, 才能被记录到

5) 问题2: vuex中的状态数据的响应式的原理?

  1. 创建了一个vm对象
  2. state中的数据都是vm的data数据(是响应式的)
  3. 组件中读取的state数据本质读取的就是data中的数据
  4. 一旦更新了state中的数据, 所有用到这个数据的组件就会自动更新
1
2
3
4
5
6
7
8
9
10
11
new Vue({
data: {
home: {
categoryList: [],
xxx: {}
},
user: {
userInfo: {}
}
}
})

6) 问题3: vuex数据刷新丢失的问题

  • 绑定事件监听: 在卸载前保存当前数据
1
2
3
4
5
6
window.addEventListener('beforeunload', () => { // 当页面刷新时, 页面卸载前的事件回调
sessionStorage.setItem('CART_LIST_KEY',
JSON.stringify(this.$store.state.shopCart.cartList))
})

window.removeEventListener('beforeunload')
  • 在初始时读取保存数据作为状态的初始值
1
cartList: JSON.parse(sessionStorage.getItem('CART_LIST_KEY')) || [],

8. 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
        • {path: ‘/xxx’, query: {name: ‘abc’, 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个数据的对象

        {

        ​ 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’})

  • 跳转携带的参数, 刷新就丢失了

    如果注册没有指定/:xxx的点位, 而跳转时通过params配置携带的参数数据, 刷新时就会丢失

    因为url中没有携带的参数数据路径

    name: ‘user’

    path: /user

    name: ‘user’

    /user/:id

    /user/2

    this.$router.push({name: ‘user’, params: {id: 2}})

    地址栏: /user (因为id没占位,所以无法刷新后会丢失,无法通过this.$route.params.id获取)

  • 路由组件能不能传递props参数?

    可以, 但只是将params/query/其它映射成props传入路由组件的

  • 编程式路由跳转到当前路由, 参数不变, 会报出错误? ==> 在做项目时有没有遇到比较难/奇怪的问题?

    ​ 编程式路由导航在重复跳转同一路径时会报错,这时需要对push或replace方法进行增强,这个这里涉及到了版本问题:

    3.1.0之前: 返回值为undefined

    push(location)

    ​ push(location, () => {}, () => {})

    3.1.0: 如果没有指定回调函数返回promise对象

    ​ push(location).then(() => {}).catch(() => {})

    ​ 问题: 如果要跳转的是路径与当前所在的路由是一样的, 且参数也没有变化, 就会返回一个失败的promise, 而我们没有catch它, 控制台就会报错

  • 说明情况:

    • 上一个项目这种操作没有这个问题
  • 后面的一个项目(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错误

      1
      2
      push('/xxx', () => {})   ===> 
      push('/xxx').catch(() => {})
    • 办法2: 重写VueRouter原型上的push方法 (比较好)

      • 1). 如果没有指定回调函数, 需要调用原本的push()后catch()来处理错误的promise
      • 2). 如果传入了回调函数, 本身就没问题, 直接调用原本的push()就可以
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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)
    }
    }

  • 说明:

    声明式路由跳转之所有没有问题, 是因为默认传入了成功的空回调函数

3) 有点难度, 但很重要的

  • 路由懒加载: ===> 预加载(vue脚手架项目已经实现了 ==> webpack)

    • () => import(‘./Home.vue’)

    • 组件单独打包, 开始不加载其打包文件, 第一次请求时才会加载 ==> 加载更快, 提高用户体验

    • 首页: 首页要加载的打包文件变小了 ==> 更快能看到界面

    • 问题: 在查看其它路由组件页面时需要临时发请求加载对应的打包文件 ==> 相对慢了一点

    • 解决: 预加载 —–vue脚手架创建的项目已经实现了

      • preload: 与主打包文件并行加载 (有一定竞争)
      • prefetch: 在主打包文件文件加载完成, 完全空闲时才去加载

      app.js

  • 缓存路由组件

    1
    2
    3
    <keep-alive>
    <router-view/>
    </keep-alive>

​ 路由离开时不销毁, 路由回来时不用重新创建 ==> 利用缓存, 切换路由更快

​ 再利用上prefetch/preload实现预获取/加载, 用户体验更佳

路由组件可以传递props参数,但不能通过路由组件的标签来传递

  • 动态添加路由

    • router.addRoutes(routes)
    • 在异步确定用户的权限路由后, 需要动态添加到路由器
    • 动态添加的路由在当次路由跳转不可见, 只有在添加后的路由跳转才可见
  • 路由守卫与权限校验

    • router.beforeEach()注册全局前置守卫
    • 统一对用户权限进行一系列的校验处理
  • history与hash路由的区别和原理

    • 区别:

      • history: 路由路径不#, 刷新会携带路由路径, 默认会出404问题, 需要配置返回首页

        • 404:

          • history有: 刷新请求时会携带前台路由路径, 没有对应的资源返回
          • hash没有: 刷新请求时不会携带#路由路径
        • 解决:

          • 开发环境: 如果是脚手架项目本身就配置好

            ==> webpack ==> devServer: {historyApiFallback : true}

            当使用 HTML5 History API 时, 所有的 404 请求都会响应 index.html 的内容

        • 生产环境打包运行:

          • 配置nginx

            1
            2
            3
            location / {
            try_files $uri $uri/ /index.html; # 所有404的请求都返回index页面
            }
      • hash: 路由路径带#, 刷新不会携带路由路径, 请求的总是根路径, 返回首页, 没有404问题

    • 原理:

      • history: 内部利用的是history对象的pushState()和replaceState() (H5新语法)
      • hash: 内部利用的是location对象的hash语法
        • 写hash路径 location.hash = ‘#/xxx’
        • 读hash路径: location.hash
        • 监视hash路径的变化: window.onhashchange = () => {}

    ​ hash模式中有hashChange事件,每次hash改变都会触发;而history中没利用的是pushState和replaceState方法,无法实时监视history的变化;onpopState事件只有在浏览器行为的前进或后退时触发,不如hash.

  • 如何让路由跳转后, 滚动条自动停留到起始位置?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    new VueRouter({ // 配置对象

    scrollBehavior (to, from, savedPosition) {
    // 对于所有路由导航,让页面滚动到顶部。
    return { x: 0, y: 0 }

    // 在按下 后退/前进 按钮时, 页面滚动停留在离开时的位置
    if (savedPosition) {
    return savedPosition // 返回前面离开时保存的位置坐标对象
    } else { // 否则直接滚动到顶部
    return { x: 0, y: 0 }
    }
    }
    })
  • 如何实现登陆后, 自动跳转到前面要访问的路由界面

    • 在全局前置守卫中, 强制跳转到登陆页面时携带目标路径的redirect参数

      1
      2
      3
      4
      5
      6
      if (userInfo.name) {
      next()
      } else {
      // 如果还没有登陆, 强制跳转到login
      next('/login?redirect='+to.path) // 携带目标路径的参数数据
      }
    • 在登陆成功后, 跳转到redirect参数的路由路径上

      1
      2
      3
      4
      await this.$store.dispatch('login', {mobile, password})
      // 成功了, 跳转到redirect路由 或 首页
      const redirect = this.$route.query.redirect
      this.$router.replace(redirect || '/')

4) 路由导航守卫的理解和使用

  • 导航守卫是什么?

    • 导航守卫是vue-router提供的下面2个方面的功能
      • 监视路由跳转 –>回调函数
      • 控制路由跳转 –> 放行/不放行/强制跳转到指定位置 next()
    • 应用
      • 在跳转到界面前, 进行用户权限检查限制(如是否已登陆/是否有访问路由权限)
      • 在跳转到登陆界面前, 判断用户没有登陆才显示
  • 导航守卫分类

    • 全局守卫: 针对任意路由跳转

      • 全局前置守卫

        1
        2
        3
        router.beforeEach((to, from, next) => {
        // ...
        })
      • 全局后置守卫

        router.afterEach((to, from) => {})

    • 路由独享守卫

      • 前置守卫

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        {
        path: '/foo',
        component: Foo,
        beforeEnter: (to, from, next) => {}
        },
        {
        path: '/ff',
        component: Foo,
        },

    • 组件守卫: 只针对当前组件的路由跳转

      • 进入

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        beforeRouteEnter (to, from, next) {
        // 在渲染该组件的对应路由被 confirm 前调用
        // 不!能!获取组件实例 `this`
        // 因为当守卫执行前,组件实例还没被创建

        next(vm => {
        // 通过 `vm` 访问组件实例
        vm.msg
        })
        },
    • 更新:

      beforeRouteUpdate (to, from, next) {}

    • 离开

      beforeRouteLeave (to, from, next) {}

面试题

1.v-if和v-show的区别

相同点:v-if与v-show都可以动态控制dom元素显示隐藏

不同点:v-if显示隐藏是将dom元素整个添加或删除,而v-show隐藏则是为该元素添加css–display:none,dom元素还在。

1.手段:v-if是动态的向DOM树内添加或者删除DOM元素;v-show是通过设置DOM元素的display样式属性控制显隐;

2.编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换;

3.编译条件:v-if是惰性的,如果初始条件为假,则什么也不做;只有在条件第一次变为真时才开始局部编译(编译被缓存?编译被缓存后,然后再切换的时候进行局部卸载); v-show是在任何条件下(首次条件是否为真)都被编译,然后被缓存,而且DOM元素保留;

4.性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

5.使用场景:v-if适合运营条件不大可能改变;v-show适合频繁切换。

注: v-show显示隐藏切换,只是会修改element style为display:””或者display:none,并不会覆盖掉或修改已存在的css属性。

2.生命周期相关

created:实例已经创建完成之后调用,在这一步,实例已经完成数据观测、属性和方法的运算,watch、event事件回调,然而,挂载阶段还没有开始,$el属性目前还不可见(此时可以通过this访问data和methods)

mounted:el被新创建的vm.$el替换,并挂载到实例上去之后调用该钩子,如果root实例挂在了一个文档内元素,当mounted被调用时vm.$el也在文档内。

activated:keep-alive组件激活时调用

beforeCreate:第一个可以看到this的生命周期,但无法通过this访问data和methods

Created : 第一个能发送请求的生命周期,但此时页面没有渲染,无法获取标签节点;

mounted: 第一个能操作dom的生命周期

3.keep-alive相关

1.keep-alive的生命周期

​ 当引入 keep-alive 的时候,页面第一次进入,钩子的触发顺序 created-> mounted-> activated,退出时触发 deactivated。当再次进入(前进或者后退)时,只触发 activated

2.作用

​ 返回dom不让其重新刷新,在vue-view外面包一层, 当引入keep-alive的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated

​ 事件挂载的方法等,只执行一次的放在 mounted 中;组件每次进去执行的方法放在 activated 中

权限管理中路由守卫跳转问题:

1.派发事件后next的放行问题:

​ 可以使用next(to.path)或者next({…to})实现放行,后者更好,原因是前者进行路由跳转时无法携带参数,参数存在于to.query内;

2.异步路由递归问题

递归时会将筛选出来的值重新赋给children,从而使第二次过滤时可能会出现有些路由当前的用户有但已被过滤的情况,所以需要在commit传递异步路由时对其进行深拷贝操作.