一文了解Vue全家桶之Vuex
一、vuex 状态管理模式
1 - 普通的数据流管理
- 单项数据流流向
2 - vuex 状态管理模式
- 集中式存储管理应用的所有组件的状态
- 以相应的规则保证状态以一种可预测的方式发生变化
二、为什么要使用vuex
- 当多个组件共享一个状态时,方便状态的管理
- 当跨层级组件之间通信时,方便数据处理
三、核心概念
1 - State
(1)state 是什么?
- 是项目中唯一的一个状态树
- 包含了项目中所有组件的共享状态值
(2)state有哪些特性?
- 一个项目中只有一个store实例
- 和 vue实例中的data类似,具有相同的响应式规则
(3)如何使用?
- 在组件中单独使用
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
// 在组件中获取状态值 count
return store.state.count
}
}
}
- 注入到根组件中使用
// 在store.js 文件中
Vue.use(Vuex);
// 在 main 函数中
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
// 在子组件中
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
- 使用mapState辅助函数
a - 方式1:使用对象的方式引入
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 01 - 箭头函数的方式
count: state => state.count,
// 02 - 普通函数需要使用this的方式 - 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
// 03 - 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
})
}
b - 方式2:使用数组的方式引入
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
c - 方式3:和其他计算属性相融
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
'count',
})
}
2 - Getter
(1)作用是什么?
- 对 state 值进行处理,依赖state进行计算,并对计算结果进行缓存
- 类似于组件中的计算属性
(2)如何使用?
- 作为普通的函数值计算返回
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '字段一', done: true },
{ id: 2, text: '字段二', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
- 作为函数返回
// 声明一个getter -> getTodoById;返回接受一个id为参数的函数
getters: {
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
// 使用时
store.getters.getTodoById(2)
(3)如何访问?
- 通过store对象访问
// 在非vue组件中
store.getters.doneTodosCount // -> 1
// 在已全局挂载store实例的vue组件中
this.$store.getters.doneTodosCount
- 使用辅助函数mapGetters
a - 使用数组的方式访问
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
])
}
}
b - 使用对象的方式访问
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
3 - Mutation
(1)Mutation的作用是什么?
- 用于对状态树中的state进行修改,
- 唯一一种修改state状态的方式
(2)含有哪些特性?
- 对于需要的状态值在一开始声明的时候最好进行一个初始化
- 注意对对象的处理,给对象动态新增属性时的处理方式,
- 使用Vue.set处理
- 返回一个新对象
- 在Mutation中只能进行同步的操作
(3)如何使用?
- 直接使用
const store = new Vuex.Store({
state: {
count: 1
},
// 声明一个 mutation , 第一个参数接受当前实例 / 模块的 state对象
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
// 组件中唤起一个Mutation
this.$store.commit('increment')
- 带参数使用
mutations: {
increment (state, n) {
state.count += n
}
}
// 在组件中使用
this.$store.commit('increment',10)
- 辅助函数使用
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
// 数组的方式使用
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
// 对象的方式使用
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
// 重点说明:使用辅助函数传参时
increment(2) => this.$store.commit('increment',2);
<div @click="increment(2)">点击</div>
(4)相关技巧?
- 使用常量代替Mutation的名称
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
4 - Action
(1)作用是什么?
- 通过在action内部提交commit从而实现改变state的目的
- 主要用于处理需要异步修改state值的情况
(2)使用方式
声明:action的第一个参数接受一个类似于store实例的对象,包含store实例的方法和函数
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
- 直接派发
this.$store.dispatch('incrementAsync')
- 带参数派发
this.$store.dispatch('incrementAsync',10);
- 辅助函数派发
import { mapActions } from 'vuex'
export default {
methods: {
// 数组的方式派发
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
// 对象的方式派发
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
(3)进阶使用 - 多个Action组合使用
- 情景一:Action 返回一个Promise,在Promise中进行判断
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
// 在组件中使用时
store.dispatch('actionA').then(() => {
// ...
})
// 在另外一个Action中使用
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
- 情景二:使用Async / Await 语法糖
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
5 - Module
(1)产生的原因
- store 作为唯一的一棵状态树,当项目比较大时,可能会导致store.js 文件过于 臃肿
- 将 store.js 以业务需求为基准进行切割,每一个文件作为一个模块,每个模块都有自己的state、Mutation、Action进行维护,也可以有自己的子模块进行维护。
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 的状态
(2)模块的命名空间
- 默认情况下,所有子模块的数据命名是注册到全局的,具有唯一性,不能重复。
- 设置模块命名空间:
const store = new Vuex.Store({
modules: {
account: {
// 在模块account中开启命名空间
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
- 在命名空间中访问全局内容:
modules: {
foo: {
namespaced: true,
// 被局部化的getters接受第三个参数和第四个参数:rootState / rootGetters
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
// 在 action 中,提供 rootState 属性
actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
- 在命名空间中注册全局内容:root:true + handler
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
- 在辅助函数中使用具有命名空间的模块
// 情景1 - 直接使用
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
// 情景2 - 传递模块路径为第一个参数
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
// 情景3 - 使用 createNamespacedHelpers
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}
四、使用技巧
1 - 在vuex中如何处理v-model的情况
- 使用value+input
// 双向绑定组件
<input :value="message" @input="updateMessage">
// vue中的使用
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
// 声明的mutation
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}
- 使用含有get和set的计算属性
// 在组件中使用v-model实现双向绑定
<input v-model="message">
// 使用含有 get / set 的计算属性
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}