目录
一、认识Vuex框架
1、关于状态管理
什么是状态管理模式
让我们从一个简单的 Vue 计数应用开始:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
这个状态自管理应用包含以下几个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
以下是一个表示“单向数据流”理念的简单示意:
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
什么情况下我应该使用vuex?
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。
2、安装与体验vuex
npm install vuex@next --save
注册HelloWorld.vue和HelloWorld1.vue组件代码如下
<template>
<h1>计数器1:{{count}}</h1>
<button @click="count++">添加</button>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
count: 0
}
}
}
</script>
<template>
<h1>计数器2:{{count}}</h1>
<button @click="count++">添加</button>
</template>
<script>
export default {
name: 'HelloWorld1',
data() {
return {
count: 0
}
}
}
</script>
此时,两个计数组件时相互独立的,各自组件只会只会影响各自的count值。如果要让这两个计数器共享一个状态,且同时操作此状态,则需要vuex出马。
vuex框架的核心是store,即仓库。简单理解,store本身就是一个容器,其内存储和管理的应用中需要多组件共享的状态。vuex中的store非常强大,其中存储的状态是响应式的,若store中的状态数据发生变化,其会自动反映到对应的组件视图上。并且,store中的状态数据并不允许开发者直接进行修改,改变store中状态数据的唯一办法是提交mutation操作,通过这样严格的而管理,可以更加方便地追踪每一个状态的变化过程。
在main.js文件中编写如下代码:
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
const app = createApp(App)
//引入createStore方法
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
//定义要共享的数据状态
state() {
return {
count: 0
}
},
//定义修改状态的方法
mutations: {
increment(state) {
state.count++
}
}
})
// 将 store 实例作为插件安装
app.use(store)
app.mount('#app')
之后就可以在组件中共享count状态了。
修改HelloWorld组件和HelloWorld1组件如下
<template>
<h1>计数器1:{{this.$store.state.count}}</h1>
<button @click="increment">添加</button>
</template>
<script>
export default {
name: 'HelloWorld',
methods: {
increment() {
this.$store.commit('increment')
}
}
}
</script>
<template>
<h1>计数器2:{{this.$store.state.count}}</h1>
<button @click="increment">添加</button>
</template>
<script>
export default {
name: 'HelloWorld1',
methods: {
increment() {
this.$store.commit('increment')
}
}
}
</script>
在组件中使用$store属性可以直接获取到store实例,此实例的state属性中存储着所有共享的状态数据,且是响应式的,可以直接绑定到组件的视图进行使用。当需要对状态进行修改是,需要调用store实例的commit方法来提交变更操作。
二、vuex中的一些核心概念
1、vuex中的状态state
状态实际上就是应用中组件需要共享的数据。在vuex中采用单一状态树来存储状态数据,也就是说我们的数据源是唯一的。在任何组件中,都可以使用如下方式来获取任何一个状态树中的数据
this.$store.state
当组件中所使用的状态数据非常多时,这种写法就会显得比较繁琐,我们也可以使用vuex中提供的mapState方法来将其直接映射成组件的计算属性进行使用。
<template>
<h1>计数器2:{{count}}</h1>
<button @click="increment">添加</button>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'HelloWorld1',
methods: {
increment() {
this.$store.commit('increment')
}
},
computed: {
...mapState(['count'])
}
}
</script>
如果组件使用的计算属性的名字与store中定义的状态名字不一致,也可以在mapState中传入对象来进行配置
...mapState({
//通过字符串映射
countData: 'count'
})
...mapState({
countData(state) {
return state.count
}
})
注意,虽然使用Vuex管理状态非常方便,但是这并不意味着需要将组件所有使用到的数据都放在store中,这会使store仓库变得巨大且容易冲突。对于那些完全是组件内部使用的数据,还是应该将其定义为局部的状态。
2、vuex中的getter
在vue中,计算属性实际上就是getter方法,当我们需要将数据处理过再进行使用时,就可以使用计算属性。对于vuex中来说,接住mapState方法,可以方便地将状态映射为计算属性,从而增加一些所需的业务逻辑。
在main.js中修改代码
getters: {
countText(state) {
return state.count + '次'
}
}
<h1>计数器1:{{this.$store.getters.countText}}</h1>
结果:
Getter方法中也支持参数的传递,这时需要让其返回一个函数,在组件对其进行使用时非常灵活
main.js中的代码修改
getters: {
countText(state) {
return (s) => {
return state.count + s
}
}
}
<h1>计数器1:{{this.$store.getters.countText('次数')}}</h1>
结果:
对于Getter方法,Vuex中还提供了一个方法用来将其映射到组件内部的计算属性中
<h1>计数器1:{{countText('次数')}}</h1>
import { mapGetters } from 'vuex';
computed: {
...mapState(['count']),
...mapGetters(['countText']),
},
3、vuex中的mutation
在vuex中,修改store中的某个状态数据的唯一方法就是提交Mutation。Mutation的定义非常简单,我们只需要将数据变动的行为封装成函数,配置在store实例的Mutations选项中即可。
//定义修改状态的方法
mutations: {
increment(state) {
state.count++
}
},
在需要触发此Mutation时,需要调用store实例的commit方法进行提交,其中使用函数名标明要提交的具体修改指令
this.$store.commit('increment')
在调用commit方法提交修改的时候,也支持传递参数,如此可以使得Mutation方法变得更加灵活。
mutations: {
increment(state, n) {
state.count += n
}
},
this.$store.commit('increment', 2)
虽然Mutation方法中参数的类型是任意的,但是我们最好使用对象来作为参数,这样做可以很方便进行多参数的传递,也支持采用对象的方式进行Mutation方法的提交。
mutations: {
increment(state, payload) {
state.count += payload.count
}
},
this.$store.commit({
type: 'increment',
count: 3
})
结果:
4、vuex中的action
Mutation的方法必须是同步的,只能同步地对数据的修改。
action与Mutation类似,不同的是,action并不会直接修改状态数据,而是对Mutation进行包装,通过提交Mutation来实现状态的改变。
action本身也是可以接受参数的,其第一个参数是默认的,是与store实例有着相同方法和属性的context上下文对象,第二个参数是自定义参数,由开发者定义,这与mutation的用法类似。在组件上使用action时,需要通过store实例对象的dispatch方法来触发。
actions: {
asyncIncrement(context, payload) {
setTimeout(() => {
context.commit('increment', {
count: 2
})
}, 3000)
}
}
this.$store.dispatch('asyncIncrement', { count: 2 })
结果:
三秒后才开始执行+2
5、vuex中的Module
module是vuex进行模块化编程的一种方式。在定义store仓库时,无论是其中的状态,还是mutation和action行为,都是共享的。vuex允许我们将store分割成模块,每个模块都有自己的state,mutations,actions,getters。
修改main.js文件
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
const app = createApp(App)
//引入createStore方法
import { createStore } from 'vuex'
const module1 = {
namespaced: true,
//定义要共享的数据状态
state() {
return {
count: 10
}
},
//定义修改状态的方法
mutations: {
increment(state, payload) {
state.count += payload.count
}
},
getters: {
countText(state) {
return (s) => {
return state.count + s
}
}
},
actions: {
asyncIncrement(context, payload) {
setTimeout(() => {
context.commit('increment', {
count: 2
})
}, 3000)
}
}
}
const module2 = {
namespaced: true,
//定义要共享的数据状态
state() {
return {
count: 20
}
},
//定义修改状态的方法
mutations: {
increment(state, payload) {
state.count += payload.count
}
},
getters: {
countText(state) {
return (s) => {
return state.count + s
}
}
},
actions: {
asyncIncrement(context, payload) {
setTimeout(() => {
context.commit('increment', {
count: 2
})
}, 3000)
}
}
}
// 创建一个新的 store 实例
const store = createStore({
modules: {
helloWorld: module1,
helloWorld1: module2
}
})
// 将 store 实例作为插件安装
app.use(store)
app.mount('#app')
<template>
<h1>计数器2:{{countData}}</h1>
<button @click="increment">添加</button>
</template>
<script>
import { mapState } from 'vuex';
export default {
name: 'HelloWorld1',
methods: {
increment() {
this.$store.commit({
type: 'helloWorld1/increment',
count: 2
})
}
},
computed: {
/* ...mapState(['count']) */
/* ...mapState({
//通过字符串映射
countData: 'count'
}) */
...mapState({
countData(state) {
return state.helloWorld1.count
}
})
}
}
</script>
<template>
<h1>计数器1:{{countText('次数')}}</h1>
<button @click="increment">添加</button>
</template>
<script>
import { mapState } from 'vuex';
import { mapGetters } from 'vuex';
export default {
name: 'HelloWorld',
methods: {
increment() {
/* this.$store.commit({
type: 'increment',
count: 3
}) */
this.$store.dispatch({
type: 'helloWorld/asyncIncrement',
count: 2
})
}
},
computed: {
/* ...mapState(['count']), */
...mapGetters('helloWorld', ['countText']),
},
}
</script>