1 .Vuex是什么
由于Vue是单向数据流,子组件内部不能直接修改从父级传递过来的数据,子组件与子组件之间无法相互传递数据。如果我们想让两个子组件之间进行通信的话,可以借助子组件 A 向父组件传值,父组件接收子组件 A 的数据后再传给 B 组件这样的方式进行通信。
但是这样会有一个问题,就是如果子组件 A 的父组件上面还有一层爷爷组件,或者还有更多祖父类型的层级,那么是不是会很麻烦。
因此,我们会想到能不能我们将一个共有的数据存在一个特定的地方,用的时候自己去拿,这样就不需要一层层传值。使用vuex来保存我们需要管理的状态值,值一旦被修改,所有引用该值的地方就会自动更新。
我们什么时候应该用到Vuex呢?
- 小应用不建议使用Vuex,因为小项目使用 Vuex 可能会比较繁琐冗余
- 中大型单页应用,因为要考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择
2. 安装vuex
vuex是一个单独的npm包,需要安装后才能使用。同时Vuex也是Vue的一个Plugin,所以需要:
npm i vuex -S
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
3. 简单案例
NumGenerater组件负责不断产生数字,产生的数字供NumShow组件读取。NumShow根据数字范围来显示不同的颜色。0-59 显示红色,59-100 显示绿色
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着应用中大部分的**状态 (state)**数据。
很显然,产生的数字就是多个组件需要共享的。这个共享的数据与单纯的全局对象是有区别的:
- Vuex的状态存储是响应式的,当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。而单纯的全局对象不会。
- Vuex中不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。而单纯的全局对象是可以直接修改的。
3.1 Step 1 两个组件
src/views/NumGenerater.vue
<template>
<div class="genNum">{{ num }}</div>
</template>
<script>
let timer = null
export default {
data() {
return {
num: 0,
}
},
mounted() {
timer = window.setInterval(() => {
let value = parseInt(Math.random() * 100)
this.num = value
}, 1000)
},
beforeDestroy() {
if (timer) {
window.clearInterval(timer)
}
},
}
</script>
<style>
.genNum {
border: 1px solid #cccccc;
width: 400px;
height: 100px;
line-height: 100px;
text-align: center;
font-size: 25px;
}
</style>
src/views/NumShow.vue
<template>
<div style="margin-top:20px;">
<div class="alert alert-success show"></div>
</div>
</template>
<script>
export default {
data() {
return {
num: 100,
}
},
}
</script>
<style>
.show {
font-size: 18px;
text-align: center;
}
</style>
src/App.vue
<template>
<div class="container" style="margin-top: 10px">
<div class="row">
<div class="col-md-6">
<num-generater />
</div>
<div class="col-md-6">
<num-show />
</div>
</div>
</div>
</template>
<script>
import NumGenerater from '@/views/NumGenerater'
import NumShow from '@/views/NumShow'
export default {
components: {
NumGenerater,
NumShow,
}
}
</script>
目前两个兄弟组件之间没有任何通信,因为共享那个数字都各自在各自的组件中。下面开始使用Vuex来实现这个数字的共享。
3.2 Step 2 创建Vuex 的Store对象
src/store/index.js
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
num: 0
},
mutations: {
change(state, newNum) {
state.num = newNum
}
}
})
export default store
这个js中,直接导出了new出来的 store对象。store中包含两个部分:
- state :其中存放状态值。这里存放了一个num,默认值为0
- mutations: 意思是变化,其中包含一些函数,Vuex 中只有这些函数才能更改 state中的数据。当store对象的commit 方法执行的时候,commit方法中会指定Vuex要调用的 mutations 函数,然后Vuex才开始调用 这些方法,此时Vuex会传递 state 对象(第一个参数)和 commit提交过来的数据(第二个参数),这样就可以对 state中的状态数据进行更新了。
3.3 Step 3 加载store对象
将Vuex的store对象配置到 Vue实例中,这样每个组件中就可以使用 $store来访问到这个store对象了
src/main.js
import Vue from 'vue'
import App from '@/App'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap'
import store from '@/store'; // 导入stroe对象 @/stroe/index.js
const vm = new Vue({
el: '#app',
store, // store对象配置到Vue实例上
render: (h) => h(App),
})
3.4 在组件中使用store
NumGenerater 组件中调用 commit 方法向 store中提交 mutation,通知stroe更改 state 中的num值
NumShow 中直接通过 this.$store.state.num 来获取值
src/views/NumGenerater.vue
...
mounted() {
timer = window.setInterval(() => {
let value = parseInt(Math.random() * 100)
this.num = value
this.$store.commit('change', value) //向store对象提交要调用的mutations
}, 1000)
},
...
src/views/NumShow.vue 组件中使用 this.$store.state.xxx 直接访问状态数据
<template>
<div style="margin-top:20px;">
<!-- 访问计算属性 cssCls 获取到 使用哪个css 类样式 -->
<div class="alert show" :class="cssCls"></div>
</div>
</template>
<script>
export default {
computed: {
cssCls() {
// 访问 Vuex Stroe 中的状态数据 num
let success = this.$store.state.num < 60
let danger = this.$store.state.num >= 60
return {
'alert-success': success, // 如果success = true, 那么div上将会应用 alert-success 类样式
'alert-danger': danger,// 如果 danger = true, 那么div上将会应用 alert-danger 类样式
}
},
},
}
</script>
程序启动后,会看到 当随机产生的数据 < 60 的时候,右侧显示为红色, >= 60 的时候,显色为绿色
也就是说 两个兄弟组件,通过共享 Vuex store 中的状态数据,实现了通信。 当Vuex store中的数据发生改变的时候,所有订阅它的组件都会响应,从而实现数据共享。但是如果使用全局变量实现共享,则不会有响应式的效果。
4. mapState
组件中可以通过 this.$store.state.xxx 来访问状态数据,可以通过辅助函数来生成计算属性,方便对状态数据的读取,比如在 NumShow 组件中有如下代码:
<script>
import { mapState } from 'vuex'
export default {
computed: mapState({
num : state=>{return state.num}
}),
}
</script>
computed 后面是计算属性,只不过计算属性 由 mapState函数来生成的。 num 是生成的计算属性的名称,后面是计算属性要执行的内容,上面等效的代码是:
<script>
import { mapState } from 'vuex'
export default {
computed: {
num(){
return this.$store.state.num
}
},
}
</script>
可以这样来理解mapState: 需要计算属性的时候,可以通过配置的方式由 mapState 函数来生成:
mapState({
计算属性名: state=>{ 计算属性要执行的逻辑 }
})
可以看到,在“计算属性要执行的逻辑” 代码块中,可以直接使用 state来获取 状态数据了。 当要使用计算属性的时候,由Vuex来调用后面的那个箭头函数,调用的时候 Vuex会传递 state到函数中。
问题来了,组件的整个计算属性都由 mapState() 来生成的话,那么组件的其它计算属性又与 Vuex无关,那这个计算属性该如何写呢?
// 如果还要定义其它计算属性该怎么写?
computed: mapState({
num : state=>{return state.num}
}),
一种写法是继续在 mapState中进行配置,大不了不使用 state参数
computed: mapState({
num : state=>{return state.num},
myProp: state=>{ 计算属性要执行的代码 }
}),
还有一种写法,这需要用到 ES6 的"展开" 运算符 “…” . 相当于把 mapState() 生成的对象 “展开” 到了 computed 指向的对象中了。
computed: {
myProp(){
计算属性要执行的代码
},
...mapState({
计算属性: state=>{ 要执行的代码 }
})
}
在使用 mapState() 函数生成计算属性的时候,如果计算属性刚好与 状态数据名称一样,还可以简化成这样:
computed: mapState({
"num",
myProp: state=>{ 计算属性要执行的代码 }
}),
这里的计算属性名称num 刚好与状态数据名称一致,则直接使用字符串“num” 即可。字符串“num” 等效的代码是 : " num : state=>{return state.num}"
如果store中的状态数据比较多,而刚好计算属性又与 状态数据名一样,就可以简化成这样:
computed: mapState(['num','v1'])
5. Getter
如果store 中的状态数据需要经过复杂的加工后取得一个值,那么可以在stroe的定义中定义getter,如在上面的 src/store/index.js文件中可以这样定义:
import Vuex from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {...},
mutations: {...},
getters: {
doneTodos: state => { 执行逻辑 }
}
})
export default store
可以把 Vuex中的Getter立即成 stroe的计算属性。
那么在组件中就可以这样来访问Getter了:
this.$store.getters.doneTodos
Getter也有辅助函数mapGetters
来快速生成组件中的计算属性
更复杂的用法参看文档
6. Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
7. Action
Action与 Mutation类似:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
不同:
mutations | actions | |
---|---|---|
触发 | this.$store.commit(名称,数据) | store.dispatch(名称,数据) |
状态数据修改 | 可直接修改 | 通过context.commit (mutation名称), 不可直接修改数据 |
同步/异步 | 只能同步执行 | 可以异步执行 |
Action 提供同样提供了 mapActions 辅助函数
8. Module模块
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态