目录
Vue组件间通信方式回顾
- 组件内的状态管理流程
组件间通信方式
- 父组件给子组件传值 (最简单的一种方式)
- 子组件给父组件传值
- 不相关组件之间传值
- 其他常见方式($ref)
简易的状态管理方案
- 上面组件间通信方式的问题
- 集中式的状态管理方案
Vuex
- 什么是Vuex?
什么情况下使用Vuex?
- 非必要的情况不要使用Vuex
- 中大型单页应用程序使用更好
- Vuex核心概念回顾
- Vuex基本结构
- State的使用
- Getter的使用
Mutation的使用
Mutation的调试
- 时光旅行
- 状态回滚
- 提交改变
- Actions的使用
Modules的使用
- 模块定义
- 模块注册
- 模块使用
- 添加命名空间
- Vuex严格模式
Vuex插件
- 插件的使用
年底最后一天来本来没有想更博客,但是留下我今年还在努力学习的印记哈哈。看了半天这居然是我第一个关于vue的博客,努力不算晚。先来对这个博客的内容进行一下梳理,有兴趣的小伙伴可以跳到自己感兴趣的地方,这个博客比较长,长文警告。
Vue组件间通信方式回顾
组件内的状态管理流程
Vue最核心的两个功能:数据驱动 和 组件化
使用基于组件化的开发,可以提高开发效率,带来更好的可维护性。
new Vue({
// state 组件内部都可以管理自己的内部状态
data () {
return {
count: 0
}
},
// view 视图,每个组件都有自己的视图,把状态绑定到视图上,当用户和视图交互的时候,可能会更改状态
template: `{{ count }}`,
// actions 行为,这里描述的是单个组件内部的状态管理,实际开发中可能多个组件可以共享状态
methods: {
increment () {
this.count++
}
}
})
这里说的状态管理 —— 是通过状态,集中管理和分发,解决多个组件共享状态的问题。
state
:状态,数据源。view
:视图。通过把状态绑定到视图呈现给用户actions
:用户和视图交互改变状态的方式
图中表明,状态绑定到视图上呈现给用户,用户通过与视图交互改变状态,之后改变了的状态再绑定到视图会后呈现给用户。
单向的数据流程很简单清晰,但是多个组件共享数据会破坏这种简单的结构。
组件间通信方式的回顾
大多数情况下,组件都不是孤立存在的,他们需要共同协作构成一个复杂的业务功能,在Vue
中,为不同的组件关系提供了不同的通信规则。
常见的组件间通信的方式有:
父组件给子组件传值 (最简单的一种方式)
- 父组件中给子组件通过相应属性传值
- 子组件通过
props
接受数据
Props Down Child
{{ title }}
Props Down Parent
子组件给父组件传值
- 子组件通过自定义事件,用
$emit
触发的时候携带参数给父组件传值 - 父组件通过
$on
注册子组件内部触发的事件,并接收传递的数据,行内可以通过$event
获取事件传递的参数 (事件处理函数中是不这么使用的)
Props Down Child
Event Up Parent
这里的文字不需要变化
不相关组件之间传值
- 也是使用自定义事件的方式,但是因为没有父子关系,所以不能通过子组件触发传值,所以这里需要使用
eventBus
,即创建一个公共的Vue
实例,这个实例的作用是作为事件总线,或者事件中心。 eventBus
:创建一个Vue
实例,这个实例并不是用来展示内容,所以没有传递任何的选项,我们使用他的目的是使用$emit
和$on
,用来触发和注册事件。- 触发事件组件:通过
$emit
触发事件,并传递参数 - 注册事件组件:通过
$on
注册事件,接收参数进行处理
// eventbus.js
import Vue from 'vue'
export default new Vue()
Event Bus Sibling01
-
+
Event Bus Sibling02
{{ msg }}
不相关组件传值
其他常见方式
$root
,$parent
,$children
,$ref
,通过这几种属性获取根组件成员,实现组件之间的通信。但这些都是不被推荐的方式。只有当项目很小,或者在开发自定义组件的时候,才会使用到。如果是大型项目的话,还是推荐使用Vuex管理状态。
下面举例通过$refs
获取子组件的状态,其他属性可以自己查看文档。
ref
的两个作用
- 在普通
HTML
标签上使用ref
,用$refs
获取到的是DOM
对象- 在组件标签上使用
ref
,用$refs
获取到的是组件实例
ref Child
ref Parent
还是一句话不建议使用,如果滥用这种方式的话可以造成状态管理的混乱。
简易的状态管理方案
上面组件间通信方式的问题
如果多个组件之间需要共享状态,使用之前演示的方式虽然都可以实现,但是比较麻烦,而且多个组件之间进行传值,很难跟踪到数据的变化。如果出现问题的话,很难定位问题。当遇到多个组件需要共享状态的时候,典型的场景如购物车,我们使用之前介绍的方案都不合适,可能会遇到以下的问题:
- 多个视图依赖同一状态,如果多层嵌套的组件依赖同一状态,使用父子组件传值可以实现,但是非常麻烦而且不易管理。
- 来自不同视图的行为需要变更同一状态,我们可以通过父子组件的方式对状态进行修改,或者通过事件机制来改变,或者同步状态的变化,以上这些方式非常的脆弱,通常会导致产生无法维护的代码。
集中式的状态管理方案
为了解决这些问题,我们把不同组件的共享状态抽取出来,存储到一个全局对象中并且将来使用的时候保证其实响应式的。这个对象创建好之后里面有全局的状态和修改状态的方法,我们的任何组件都可以获取和通过调用对象中的方法修改全局对象中的状态 (组件中不允许直接修改对象的state
状态属性)。
把多个组件的状态放到一个集中的地方存储,并且可以检测到数据的更改,这里先不使用Vuex
,我们自己先进行一个简单的实现。
- 创建一个全局的
store.js
集中式的状态管理,所有的状态都在这里。这个模块中导出了一个对象,这对象就是状态仓库且全局唯一的对象,任何组件都可以导入这个模块使用这里面有
state
,还有actions
,state
是用来存储状态,actions
是用户交互更改视图用的。还有一个debug
的属性,方便开发调试。
// store.js
export default {
debug: true,
state: {
user: {
name: 'xiaomao',
age: 18,
sex: '男'
}
},
setUserNameAction (name) {
if (this.debug) {
console.log('setUserNameAction triggered:', name)
}
this.state.user.name = name
}
}
- 在组件中导入
componentA
user name: {{ sharedState.user.name }}
componentB
user name: {{ sharedState.user.name }}
上面组件A
和组件B
都共享了全局的状态,并且用户都可以更改状态。调试的时候,按A
组件的按钮两者都变成了componentA
,点B
组件的按钮两者都变成了componentB
。
我们不在组件中直接修改状态的值而是通过调用store
的actions
来修改值,这样记录的好处是: 能够记录store
中所以state
的变更,当可以实现记录store
的state
的变更时候,就可以实现高级的调试功能。例如:timeTravel
(时光旅行)和历史回滚功能。
刚才使用的store
,其实就类似于Vuex
的仓库。
当项目比较复杂,多个组件共享状态的时候,使用组件间通信的方式比较麻烦,而且需要维护。这个时候我们可以使用集中式的状态解决方案 —— Vuex
Vuex
好的终于进入了主题~~
什么是Vuex?
- Vuex官网
Vuex
是专门为Vue.js
设计的状态管理库,从使用的角度其实就是一个JavaScript
库- 它采用集中式的方式存储需要共享的数据,如果状态特别多的话不易管理,所以
Vuex
还提供了一种模块的机制,按照模块管理不同的状态 - 它的作用是进行状态管理,解决复杂组件通信,数据共享
Vuex
也集成到Vue
的官方调试工具devtools extension
,提供了time-travel
时光旅行、历史回滚、状态快照、导入导出等高级调试功能
什么情况下使用Vuex?
非必要的情况不要使用Vuex
Vuex
可以帮助我们管理组件间共享的状态,但是在项目中使用Vuex
的话,我们需要了解Vuex
中带来的新的概念和一些API
,如果项目不大,并且组件间共享状态不多的情况下,这个时候使用Vuex
给我们带来的益处并没有付出的时间多。此时使用简单的 store 模式 或者其他方式就能满足我们的需求。
中大型单页应用程序使用更好
中大型单页应用程序中,使用Vuex
可以帮我们解决多个视图依赖同一状态、来自不同视图的行为需要变更同一状态的问题。建议符合这些情况的业务,使用Vuex
进行状态管理,会给我们提供更好的处理组件的状态,带来的收益会更好些。例如典型案例:购物车。
注意:不要滥用
Vuex
,否则会让业务变得更复杂。
Vuex核心概念回顾
下面这张图展示了Vuex
的核心概念并且展示了Vuex
的整个工作流程
- Store:仓库,
Store
是使用Vuex
应用程序的核心,每个应用仅有一个Store
,它是一个容器,包含着应用中的大部分状态,当然我们不能直接改变Store
中的状态,我们要通过提交Mutations
的方式改变状态。 - State:状态,保存在
Store
中,因为Store
是唯一的,所以State
也是唯一的,称为单一状态树,这里的状态是响应式的。 - Getter:相当于
Vuex
中的计算属性,方便从一个属性派生出其他的值,它内部可以对计算的结果进行缓存,只有当依赖的状态发生改变的时候,才会重新计算。 - Mutation:状态的变化必须要通过提交
Mutation
来完成。 - Actions:与
Mutation
类似,不同的是可以进行异步的操作,内部改变状态的时候都需要改变Mutation
。 - Module:模块,由于使用的单一状态树让所有的状态都会集中到一个比较大的对象中,应用变得很复杂的时候,
Store
对象就会变得相当臃肿,为了解决这些问题Vuex
允许我们将Store
分割成模块,每个模块拥有自己的State
,Mutation
,Actions
,Getter
,甚至是嵌套的子模块。
Vuex基本结构
使用vue-cli
创建项目的时候,如果选择了Vuex
,会自动生成Vuex
的基本结构。
// store.js
import Vue from 'vue'
// 导入Vuex插件
import Vuex from 'vuex'
// 通过use方法注册插件
// 插件内部把Vuex的Store注入到了Vue的实例上
Vue.use(Vuex)
// 创建了Vuex的Store对象并且导出
export default new Vuex.Store({
state: {
...
},
mutations: {
...
},
actions: {
...
},
modules: {
...
}
// 如果有需要还可以有getters
})
//App.js
// 导入store对象
import store from './store'
new Vue({
router,
// 在初始化Vue的时候传入store选项,这个选项会被注入到Vue实例中
// 我们在组件中使用的this.$store就是在这个地方注入的
store,
render: h => h(App)
}).$mount('#app')
State的使用
- 下载项目模板vuex-sample-temp ,
npm install
下载依赖,在store/index.js
中定义两个state
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
msg: 'hello vue'
},
mutations: {
},
actions: {
},
modules: {
}
})
- 在
App.vue
中使用state
,然后使用npm run serve
查看结果
Vuex - Demo
count: {{ $store.state.count}}
msg: {{ $store.state.ms }}
- 每次使用变量都要前面写
$store.state 很是麻烦,所以这里使用
`Vuex内部提供的
myState`的函数,会帮我们生成状态对应的计算属性
Vuex - Demo
count: {{ count }}
msg: {{ msg }}
- 上面的方法比较简洁但是如果这个组件中本身就有
count
或者msg
属性,就会造成名称冲突,这个时候需要设置别名。
Vuex - Demo
count: {{ num }}
msg: {{ message }}
Getter的使用
Vuex
中的getter
就相当于组件中的计算属性,如果想要对state
的数据进行简单的处理在展示,可以使用getter
这里用Vuex
的getter
处理而不是用组件中的计算属性是因为状态本身属于Vuex
,应该在其内部处理
- 在
store.js
中设置getters
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
msg: 'hello vue'
},
// 与计算属性的写法一致
getters: {
reverseMsg (state) {
return state.msg.split('').reverse().join('')
}
},
mutations: {
},
actions: {
},
modules: {
}
})
- 在
App.vue
中使用
Vuex - Demo
reverseMsg: {{ $store.getters.reverseMsg }}
- 同样那样引用过于麻烦,那么和
mapState
一样,使用内部的mapGetters
,也是将其映射到组件的计算属性,其用法和mapState
一样,也可以为了避免冲突使用对象设置别名。
Vuex - Demo
reverseMsg: {{ reverseMsg }}
Mutation的使用
状态的修改必须提交Mutation
,Mutation
必须是同步执行的。
- 当用户点击按钮的时候,
count
值进行增加,先在store.js
中写Mutation
函数
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
msg: 'hello vue'
},
mutations: {
// 增加函数,接收两个参数
// 第一个state状态
// 第二个是payload载荷,payload是mutations的时候提交的额外参数,可以是对象,这里传递的是数字
increate (state, payload) {
state.count += payload
}
},
actions: {
},
modules: {
}
})
- 在
App.vue
中设置按钮,并注册事件
Vuex - Demo
count: {{ $store.state.count }}
Mutation
- 点击按钮的时候,
count
的值每次+2
- 下面进行写法优化,使用
map
方法将当前的mutation
映射到methods
中,其依旧会返回一个对象,这个对象中存储的是mutation
中映射的方法
Vuex - Demo
count: {{ $store.state.count }}
Mutation
Mutation的调试
运行到4
之后,这时看一下devtools
看一下时光旅行和历史回滚,下面是初始状态
点一下按钮之后就增加了一个记录,还显示了改变之后的数据
如果数据不对,可以进行调试。
时光旅行
然后多点几下,进行时光旅行。
点击按钮之后,状态就变成了之前那个状态,这个功能也是为了方便调试
状态回滚
这个图标就是状态回滚
点击之后,代码就回到了没有执行这一步的状态
提交改变
下面那个按钮的意思是将这次提交作为最后一次提交
点击之后,base State
变成了那次的状态,其他的状态以这个作为起始点
Actions的使用
如果有异步的修改,需要使用actions
,在actions
中可以执行异步操作,当异步操作结束后,如果需要更改状态,还需要提交Mutation
。
- 在
actions
中添加方法increateAsync
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
msg: 'hello vue'
},
mutations: {
increate (state, payload) {
state.count += payload
}
},
actions: {
// actions中的方法有两个参数:第一个参数是context上下文,这个对象中有state,commit,getters等成员,第二个参数是payLoad
increateAsync (context, payLoad) {
setTimeout(() => {
context.commit('increate', payLoad)
}, 2000)
}
},
modules: {
}
})
- 在
App.vue
中使用dispatch
,actions
的方法都要用这个
Vuex - Demo
count: {{ $store.state.count }}
Actions
- 进行优化,这个时候引入
mapActions
Vuex - Demo
count: {{ $store.state.count }}
Actions
Modules的使用
模块可以让我们把单一状态树拆分成多个模块,每个模块都可以拥有自己的state
,mutation
,action
,getter
甚至嵌套子模块。
模块定义
在store
文件夹中,创建一个modules
文件夹,里面每一个js
文件就是一个模块,下面是每一个模块的定义格式
const state = {
products: [
{ id: 1, title: 'iPhone 11', price: 8000 },
{ id: 2, title: 'iPhone 12', price: 10000 }
]
}
const getters = {}
const mutations = {
setProducts (state, payload) {
state.products = payload
}
}
const actions = {}
export default {
namespaced: false,
state,
getters,
mutations,
actions
}
模块注册
- 先导入这个模块
import products from './modules/products'
import cart from './modules/cart'
- 后来在
modules
选项中注册,注册之后这里会把模块挂载到store
的state
中,这里可以通过store.state.products
访问到products
模块中的成员,还把的模块中的mutation
成员记录到了store
的内部属性_mutation
中,可以通过commit
直接提交mutation
。
export default new Vuex.Store({
state: {},
getters: {},
mutations: {},
actions: {},
modules: {
products,
cart
}
})
模块使用
- 在
App.vue
中使用,state
就点出来,mutation
还是用commit
方法
Vuex - Demo
Modules
products: {{ $store.state.products.products }}
添加命名空间
因为每个模块中的mutation
是可以重名的,所以推荐使用命名空间的用法,方便管理。
- 在开启命名空间的时候,在模块的导出部分添加
namespaced
const state = {}
const getters = {}
const mutations = {}
const actions = {}
export default {
// true就是开启,false或者不写就是关闭
namespaced: false,
state,
getters,
mutations,
actions
}
- 使用的时候在
App.vue
中要设置state
是模块中出来的,如果没有命名空间,就是全局的state
的。
Vuex - Demo
products: {{ products }}
Vuex严格模式
所有的状态变更必须提交mutation
,但是如果在组件中获取到$store.state.msg
进行修改,语法层面没有问题,却破坏了Vuex
的约定,且devTools
也无法跟踪到状态的修改,开启严格模式之后,如果在组件中直接修改state
,会报错。
- 在
index.js
,初始化Store
的时候开启严格模式
export default new Vuex.Store({
strict: true,
state: {
...
},
...
}
- 在
App.vue
中使用直接赋值的语句
Vuex - Demo
strict
- 点击按钮内容改变,但是控制台会抛出错误
注意:不要在生产环境开启严格模式,因为严格模式会深度检测状态树,会影响性能。在开发模式中开启严格模式,在生产环境中关闭严格模式
export default new Vuex.Store({ strict: process.env.NODE_ENV !== 'production', state: { ... }
Vuex插件
Vuex
插件就是一个函数,接收一个store
的参数- 在这个函数中可以注册函数让其在所有的
mutations
结束之后再执行
插件的使用
- 插件应该在创建
Store
之前去创建 subscribe
函数- 作用是去订阅
store
中的mutation
- 他的回调函数会在每个
mutation
之后调用 subscribe
会接收两个参数,第一个是mutation
,还可以区分模块的命名空间,第二个参数是state
,里面是存储的状态
- 作用是去订阅
- 定义插件
// 这个函数接收store参数
const myPlugin = store => {
// 当store初始化后调用
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
// type里面的格式是 "模块名/state属性"
// state 的格式为 { 模块一, 模块二 }
})
}
- 在
Store
中注册插件
const store = new Vuex.Store({
//...
plugins: [myPlugin]
})