Vuex

Vuex是什么?

Vuex是一个专为Vue.js开发的状态管理模式+库,它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

最简单的Store

安装Vuex后,先创建一个store,仅需要提供一个初始state对象和一些mutation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @/src/utils/store.js
import { createStore } from 'vuex'
const store = createStore({
state() {
return {
count: 0
}
},
mutations: {
increment(state) {
state.count++
}
}
})
export default store;

然后在main.js中使用它:

1
2
3
4
5
6
import { createApp } from 'vue'
import App from './App.vue'
import store from './utils/store'

const app = createApp(App)
app.use(store).mount('#app')

现在可以通过 store.state 来获取状态对象,并通过 store.commit 方法触发状态变更:

1
2
store.commit('increment')
console.log(store.state.count) // -> 1

在组件中可以通过this.$store访问store

1
2
3
4
5
6
methods: {
increment() {
this.$store.commit('increment')
console.log(this.$store.state.count)
}
}

State

在Vue组件中获得Vuex状态

Vuex通过Vue的插件系统将store实例从根组件中注入到所有的子组件里。且子组件能通过this.$store访问到。

1
2
3
4
5
6
7
8
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}

mapState辅助函数

当一个组件需要获取多个状态时,将这些状态都声明为计算属性会显得冗余,我们可以使用mapState函数生成计算属性:

1
2
3
4
5
6
7
import { mapState } from 'vuex'

export default {
computed: mapState({
count: state => state.count,
})
}

当要映射的计算属性与state的子节点名称相同时,我们可以给mapState传一个字符串数组:

1
2
3
4
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])

使用对象展开运算符,我们可以把mapState返回的对象与其他局部计算属性一起混用:

1
2
3
4
5
6
7
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}

Getter

Getter可以认为是store的计算属性。

Getter接受state为其第一个参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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会暴露为 store.getters 对象,可以以属性的形式访问这些值。

1
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

可以在组件中使用Getter:

1
2
3
4
5
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}

通过方法访问

Getter可以返回一个函数来实现给Getter传参。

1
2
3
4
5
getters: {
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
1
store.getters.getTodoById(2)

mapGetters辅助函数

mapGetters 辅助函数仅仅是将store中的getter映射到局部计算属性:

1
2
3
4
5
6
7
8
9
10
import { mapGetters } from 'vuex'

export default {
computed: {
...mapGetters([
'doneTodosCount',
'anotherGetter',
])
}
}

如果想将一个getter属性另取一个名字,使用对象形式:

1
2
3
4
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})

Mutation

更改Vuex的store中的状态的唯一方法是提交mutation。一个mutation有一个字符串的数据类型和一个回调函数,回调函数就是修改state的地方。mutationstate做为它的第一个参数:

1
2
3
4
5
6
7
8
9
10
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
state.count++
}
}
})

调用mutation时,要调用store.commit方法:

1
2
3
4
5
6
7
8
// 将type写成字符串的写法,只会把第二个参数当成payload
store.commit('increment',payload)

// 使用包含type属性的对象的写法,会将一整个对象当成payload
store.commit({
type: 'increment',
amount: 10
})

提交Payload

可以向store.commit传入额外的参数,即mutation的payload:

1
2
3
4
5
mutations: {
increment (state, n) {
state.count += n
}
}
1
store.commit('increment', 10)

在组件中提交Mutation

可以在组件中使用 this.$store.commit('xxx') 提交mutation,或者使用 mapMutations辅助函数将组件中的methods映射为store.commit 调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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')`
})
}
}

Action

Action类似于mutation,不同在于Action提交的是mutation,并且Action可以包含异步操作。注册一个简单的Action

1
2
3
4
5
actions: {
increment (context) {
context.commit('increment')
}
}

Action函数接受一个与store实例具有相同方法和属性的context对象,因此可以在action里通过context.commit提交一个mutation,或者通过context.statecontext.getters来获取 state 和 getters。

可以使用ES6的参数解构来简化代码

1
2
3
increment = ({ commit }) => {
commit('increment')
}

分发Action

Action通过store.dispatch触发:

1
store.dispatch('increment')

Action类似Mutation,支持同样的Payload方式和对象方式分发:

1
2
3
4
5
6
7
8
9
10
// 以payload形式分发
store.dispatch('incrementAsync', {
amount: 10
})

// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})

在组件中分发Action

在组件中可以使用this.$store.dispatch分发action,或者使用mapActionsaction映射到组件的methods中。

组合Action

store.dispatch可以处理被触发的action的处理函数返回的Promise,并且store.dispatch仍然返回Promise:

1
2
3
4
5
6
7
8
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}

接下来可以链式调用:

1
2
3
store.dispatch('actionA').then(() => {
// ...
})

在另一个Action中也可以:

1
2
3
4
5
6
7
8
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}

结合async/await,可以优化结合action的代码:

1
2
3
4
5
6
7
8
9
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}

表单处理

在表单上对属于Vuex的store使用v-model会比较棘手:

1
<input v-model="obj.message">

因为storestate只能通过触发mutation改变,直接通过v-model改变,会抛出错误。用常规的思维解决这个问题的方法是:给input表情绑定value,然后监听input或者change事件,在事件回调中触发mutation使事件改变。

这样做来修改数据太麻烦了,并且损失了v-model中很多方便的特性,下面是另一种修改的方法:在计算属性中提供set方法,然后再set方法中触发mutation

1
<input v-model="message">
1
2
3
4
5
6
7
8
9
10
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}