Vuex
是一个专为 Vue.js 应用程序开发的 状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
将多个组件共享的变量全部存储在一个对象里面,供所有组件共享,相当于全局变量。而且数据发生改变时,会 响应式
地更新所有组件,相当于订阅模式。
一般情况下,我们会在 Vuex 中存放一些需要在多个界面共享的信息。比如用户的登录状态
、用户名称
、头像
、地理位置信息
、商品的收藏
、购物车中的物品
等,这些状态信息,我们可以放在统一的地方,对它进行保存和管理。
本章只做学习记录,详尽的内容一定要去官网查看api文档: Vuex
npm install vuex@next --save
1、src下创建一个叫 store 的文件夹,并创建 index.js,创建并暴露一个 store
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
//相当于data,只是它是全局的,所有组件都能访问
//其实,我们可以通过获取state.count直接修改其属性值
//但是,state是全局的,多个组件共享的属性,那么对该属性值的操作,就不应该放在具体的组件中
//从而,引出mutations
state() {
return {
count: 0
}
},
//mutations中编写对共享数据state的修改逻辑
mutations: {
increment(state) {
state.count++
}
}
})
export default store
仅需要提供一个初始 state
对象和一些 mutation
2、创建需要的组件
<template>
this is About!
<span>这里访问store的count属性:{{ $store.state.count }}</span>
</template>
在 Vue 组件中, 可以通过 this.$store
访问 store 实例。
import { createApp } from 'vue'
import App from './App.vue'
import store from '@/store'
createApp(App).use(store).mount('#app')
4、测试
如果需要修改 store.state.count 属性值,有两种方式。
方式一:直接获取 store 实例,并且修改 state 属性值 (不推荐)
<template>
this is About!
<span>这里访问store的count属性:{{ $store.state.count }}</span>
<button @click="change"> change </button>
</template>
<script>
export default {
name: 'About',
methods: {
change() {
this.$store.state.count++;
}
}
}
</script>
方式二:通过 mutation
<template>
this is About!
<span>这里访问store的count属性:{{ $store.state.count }}</span>
<button @click="change"> change </button>
</template>
<script>
export default {
name: 'About',
methods: {
change() {
//this.$store.state.count++;
this.$store.commit('increment')
}
}
}
</script>
注意:
计算属性 conputed
中返回即可;触发变化也仅仅是在组件的 methods
中提交 mutation。存储在 Vuex 中的数据和 Vue 实例中的 data
遵循相同的规则,由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在 计算属性 computed
中返回某个状态:
<template>
this is About!
<span>这里访问store的count属性:{{ getCount }} </span>
</template>
<script>
export default {
computed: {
getCount(){
return this.$store.state.count
}
}
}
</script>
每当 this.$store.state.count
变化的时候,都会重新求取计算属性,并且触发更新相关联的 DOM。
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性。
<template>
this is About! <br/>
<span>这里访问store的count属性:{{ getCount }}</span> <br/>
<span>这里访问store的count属性:{{ countAlias }}</span> <br/>
<span>这里访问store的count属性:{{ countPlusLocalState }}</span>
</template>
<script>
import { mapState } from 'vuex'
export default {
data(){
return{
localCount: 10
}
},
computed: mapState({
// 箭头函数可使代码更简练
getCount: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
};
</script>
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组。
<template>
this is About! <br/>
<span>这里访问store的count属性:{{ count }}</span> <br/>
</template>
<script>
import { mapState } from 'vuex'
export default {
data(){
return{
localCount: 10
}
},
computed: mapState( ['count'] )
};
</script>
mapState
函数返回的是一个 对象。我们如何将它与局部计算属性混合使用呢?通常,我们使用 对象展开运算符 ...
将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性
。这将极大地简化写法:
<template>
this is About! <br/>
<span>这里访问store的count属性:{{ count }}</span> <br/>
<span>这里访问组件本地的localCount属性:{{ localComputed }}</span> <br/>
</template>
<script>
import { mapState } from 'vuex'
export default {
data(){
return{
localCount: 10
}
},
computed: {
...mapState( ['count'] ),
localComputed () {
return this.localCount + 10
}
}
};
</script>
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。
如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数,一般做法组件内使用计算属性 computed。
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它 —— 无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义 getter
(可以认为是 store 的计算属性)。
Getter 接受 state 作为其第一个参数:
1、store/index.js 中定义 getters
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state: {
todos: [
{ id: 1, text: '吃饭', done: true },
{ id: 2, text: '工作', done: false }
]
},
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
},
//接受其他 getter 作为第二个参数
doneTodosCount (state, getters) {
return getters.doneTodos.length
}
}
})
export default store
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter
作为第二个参数:
getters: {
// ...
doneTodosCount (state, getters) {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性。
2、组件使用 store 中的 getter 属性
<template>
this is About! <br/>
<span>这里访问store的count属性:{{ count }}</span> <br/>
<span>这里访问组件本地的localCount属性:{{ localComputed }}</span> <br/>
<span>这里访问store中的 getter 属性:{{ doneTodos }}</span> <br/>
<span>这里访问store中的 getter 属性:{{ doneTodosCount }}</span> <br/>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
export default {
data(){
return{
localCount: 10
}
},
computed: {
...mapState( ['count'] ),
...mapGetters( ['doneTodos', 'doneTodosCount'] ),
localComputed () {
return this.localCount + 10
}
}
};
</script>
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。它会接受 state 作为第一个参数:
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
组件不能直接调用一个 mutation 处理函数。需要调用 store.commit
方法:
store.commit('increment')
你可以向 store.commit
传入额外的参数,即 mutation 的 载荷(payload):
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
提交 mutation 的另一种方式是直接使用包含 type
属性的对象:
store.commit({
type: 'increment',
amount: 10
})
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = createStore({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能
// 来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// 修改 state
}
}
})
一条重要的原则就是要记住 mutation 必须是同步函数。
使用 mapMutations
辅助函数将组件中的 methods
映射为 this.$store.commit
调用。
1、store/index.js 中定义 mutations
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state, payload) {
state.count += payload.count
}
}
})
export default store
注:mutations 内部方法间不能相互调用,可以使用 action
编写调用逻辑,或者组件 methods
内定义方法编写调用逻辑。
2、组件使用 store 中的 mutations属性
<template>
this is About! <br/>
<span>这里访问store的count属性:{{ count }}</span> <br/>
<button @click="add"> add </button> <br/>
<button @click="increment({count: 20})"> increment </button>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
computed: {
...mapState( ['count'] )
},
methods: {
//commit 调用 store 方法
add() {
this.$store.commit('increment', {count: 10})
},
//mapMutations映射,相当于将 `this.increment()` 映射为 `this.$store.commit('increment')`,直接调用increment即可
...mapMutations( ['increment'] )
}
};
</script>
Action 类似于 mutation,不同在于:
相当于 SpringMvc 的 Controller 层,进行 mutation 的调用编排。注册一个简单的 action:
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。当我们在之后介绍到 Modules
时,你就知道 context 对象为什么不是 store 实例本身了。
还可以直接解构来简化代码(特别是我们需要调用 commit 很多次的时候):
actions: {
increment ({ commit }) {
commit('increment')
}
}
Action 通过 store.dispatch
方法触发:
store.dispatch('increment')
还记得 mutation 必须同步执行 这个限制么?Action 就不受约束!我们可以在 action 内部执行 异步 操作:
1、store/index.js 中定义 actions
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
},
increment2(state) {
state.count++
}
},
actions: {
increment(context) {
//1、action 可以组织 mutation 的调用逻辑
context.commit('increment')
context.commit('increment2')
},
incrementAsync({ commit }) {
//2、action 内部执行异步操作
setTimeout(() => {
commit('increment')
}, 1000)
}
}
})
export default store
2、组件使用 store 中的 actions 属性
<template>
this is About! <br/>
<span>这里访问store的count属性:{{ count }}</span> <br/>
<button @click="add"> add </button> <br/>
<button @click="add2"> add2 </button> <br/>
</template>
<script>
import { mapState, mapGetters, mapMutations } from 'vuex'
export default {
computed: {
...mapState( ['count'] )
},
methods: {
add() {
this.$store.dispatch('increment')
},
add2() {
this.$store.dispatch('incrementAsync')
}
}
};
</script>
mapActions
辅助函数将组件的 methods
映射为 this.$store.dispatch
调用。
<template>
this is About! <br/>
<span>这里访问store的count属性:{{ count }}</span> <br/>
<button @click="increment"> add </button> <br/>
<button @click="incrementAsync"> add2 </button> <br/>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
...mapState( ['count'] )
},
methods: {
...mapActions( ['increment','incrementAsync'] )
}
};
</script>
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
首先,你需要明白 store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise
,并且 store.dispatch
仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
你可以在组件内调用:
this.$store.dispatch('actionA').then(() => {
// ...
})
也可以在另外一个 action 中调用:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后,如果我们利用 async / await
,我们可以如下组合 action:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)
。每个模块拥有自己的 state、mutation、action、getter。
const moduleA = {
state: () => ({ count: 0 }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
注意: 根模块访问子模块的state,通过store.state.a.count
。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
根级别的 action、mutation 可以访问子模块的 action、mutation。
1、对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
2、对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
3、对于模块内部的 action,局部状态通过 context.state
暴露出来,根节点状态则为 context.rootState
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}