四、在Vue组件中使用Vuex
1、注入store
使用Vue3、Vuex4版本,通过如下方式向注入store:
import { createApp } from 'vue';
import App from './App.vue';
import {createStore} from 'vuex';
const store = createStore({
state: {
counter: 0
},
getters: {
counter10times(state) {
return state.counter * 10;
}
},
mutations: {
increaseCounter(state) {
state.counter++;
}
},
modules: {
a: {
namespaced: true,
state: {aName: 'A·a'},
getters: {
aFirstName(state) {
return state.aName.split('·')[0];
}
},
mutations: {
changeAName(state) {
state.aName = 'A-a';
}
},
actions: {
callChangeCNameAsync({dispatch}) {
// 触发子模块的action,相对与自身的路径
setTimeout(() => {
dispatch('c/changeCNameAsync');
}, 500);
}
},
modules: {
c: {
namespaced: true,
state: {cName: 'C·c'},
getters: {
cFirstName(state) {
return state.cName.split('·')[0];
}
},
mutations: {
changeCName(state, payload) {
state.cName = `C-c-${payload.suffix}`;
}
},
actions: {
changeCNameAsync({commit, rootState}) {
setTimeout(() => {
// 提交其他模块的mutation,mutation是全局的
commit('increaseCounter', null, {root: true});
// 提交局部模块的mutation,不需要加前缀
commit('changeCName', {
suffix: rootState.counter
});
}, 500);
}
}
}
}
},
b: {
namespaced: true,
state: {bName: 'B·b'},
getters: {
bNewName(state, getters, rootState, rootGetters) {
// 局部state
const bName = state.bName.split('·')[0];
// 其他模块的getter
const cFirstName = rootGetters['a/c/cFirstName'];
// 其他模块的state
const aName = rootState.a.aName;
return `${bName} ${cFirstName} ${aName}`;
}
},
mutations: {
changeBName(state) {
state.bName = 'B-b';
}
}
}
}
});
createApp(App).use(store).mount('#app');
将加了命名空间的store注入到Vue组件树中。这样在所有的Vue组件中,都能够通过this.$store方式访问store。
Vue组件通过computed属性来监听store的数据变化。
看下面的示例,computed依赖了this.$store里面的一些模块的state和getters,并将计算结果展示在界面上。
<template>
<div>
{{counter}}
{{bName}}
{{cFirstName}}
</div>
</template>
<script>
export default {
computed: {
counter() {
return this.$store.state.counter;
},
bName() {
return this.$store.state.b.bName;
},
cFirstName() {
return this.$store.getters['a/c/cFirstName'];
}
}
}
</script>
<style>
#app {
margin-top: 60px;
}
</style>
Vue组件中改变状态的示例:
可以看到Vue组件中通过methods方法调用this.$store的commit和dispatch方法来提交修改和触发action。
/**
* @file main.js
*/
import { createApp } from 'vue';
import App from './App.vue';
import {createStore} from 'vuex';
const store = createStore({
state: {
counter: 0
},
getters: {
counter10times(state) {
return state.counter * 10;
}
},
mutations: {
increaseCounter(state) {
state.counter++;
}
},
modules: {
a: {
namespaced: true,
state: {aName: 'A·a'},
getters: {
aFirstName(state) {
return state.aName.split('·')[0];
}
},
mutations: {
changeAName(state) {
state.aName = 'A-a';
}
},
actions: {
callChangeCNameAsync({dispatch}) {
// 触发子模块的action,相对于自身的路径
setTimeout(() => {
dispatch('c/changeCNameAsync');
}, 500);
}
},
modules: {
c: {
namespaced: true,
state: {cName: 'C·c'},
getters: {
cFirstName(state) {
return state.cName.split('·')[0];
}
},
mutations: {
changeCName(state, payload) {
state.cName = `C-c-${payload.suffix}`;
}
},
actions: {
changeCNameAsync({commit, rootState}) {
setTimeout(() => {
// 提交其他模块的mutation,mutation是全局的
commit('increaseCounter', null, {root: true});
// 提交局部模块的mutation,不需要加前缀
commit('changeCName', {
suffix: rootState.counter
});
}, 500);
}
}
}
}
},
b: {
namespaced: true,
state: {bName: 'B·b'},
getters: {
bNewName(state, getters, rootState, rootGetters) {
// 局部state
const bName = state.bName.split('·')[0];
// 其他模块的getter
const cFirstName = rootGetters['a/c/cFirstName'];
// 其他模块的state
const aName = rootState.a.aName;
return `${bName} ${cFirstName} ${aName}`;
}
},
mutations: {
changeBName(state) {
state.bName = 'B-b';
}
}
}
}
});
createApp(App).use(store).mount('#app');
/**
* @file App.vue
*/
<template>
<div>
{{counter}}
{{bName}}
{{cFirstName}}
<button @click="modifyBName">修改b的name</button>
<button @click="modifyCName">修改c的name</button>
</div>
</template>
<script>
export default {
computed: {
counter() {
return this.$store.state.counter;
},
bName() {
return this.$store.state.b.bName;
},
cFirstName() {
return this.$store.getters['a/c/cFirstName'];
}
},
methods: {
modifyBName() {
this.$store.commit('b/changeBName');
},
modifyCName() {
this.$store.dispatch('a/callChangeCNameAsync');
}
}
}
</script>
<style>
#app {
margin-top: 60px;
}
</style>
2、辅助函数
我们知道我们使用Vuex时候,通过computed绑定store的state和getters的数据,通过methods中调用this.$store的commit和dispatch方法来改变状态。
但是每次都要写this.$store,当需要绑定的数据多的时候会比较繁杂,因此Vuex提供了辅助函数来简化代码。辅助函数包括
-
mapState
-
mapGetters
-
mapMutations
-
mapActions
其中mapState和mapGetters将映射到computed属性中,mapMutations和mapActions映射到methods属性中。
用法见下面示例:
<template>
<div>
{{counter}}
{{bName}}
{{counter10times}}
{{cFirstName}}
<button @click="modifyBName">修改b的name</button>
<button @click="modifyCName">修改c的name</button>
</div>
</template>
<script>
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'
export default {
computed: {
...mapState({
// 将this.$store.state.counter映射为counter
counter: state => state.counter,
// 也可以这样实现
// counter: 'counter',
bName: state => state.b.bName
}),
// 全局的getters
...mapGetters(['counter10times']),
// 也可以这样实现,指定组件中的数据名称
// ...mapGetters({
// counter10times: 'counter10times'
// }),
// 子模块的getters
...mapGetters({
cFirstName: 'a/c/cFirstName'
}),
// 带有命名空间的子模块也可以这样实现映射,在方法多的时候可以简化代码
// ...mapGetters('a/c', [
// 'cFirstName'
// ])
},
methods: {
// 映射mutations到方法
...mapMutations({
modifyBName: 'b/changeBName'
}),
// 也可以这样实现
// ...mapMutations('b', {
// modifyBName: 'changeBName'
// }),
// 带有命名空间的子模块映射到组件的方法
...mapActions('a', {
modifyCName: 'callChangeCNameAsync'
}),
}
}
</script>
<style>
#app {
margin-top: 60px;
}
</style>
3、组件之间共享数据
Vuex在组件共享数据场景的一个简单示例:
有两个子组件bChild和cChild,它们直接从store中获取数据并渲染。在根组件App.vue中修改store中的数据,可以看到子组件会相应数据更新,展示最新的数据。
<template>
<div>
{{counter}}
{{counter10times}}
<b-child></b-child>
<c-child></c-child>
<button @click="modifyBName">修改b的name</button>
<button @click="modifyCName">修改c的name</button>
</div>
</template>
<script>
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'
import bChild from './components/bChild';
import cChild from './components/cChild';
export default {
computed: {
...mapState({
counter: state => state.counter,
// 也可以这样实现
// counter: 'counter',
}),
// 全局的getters
...mapGetters(['counter10times']),
// 也可以这样实现,指定组件中的数据名称
// ...mapGetters({
// counter10times: 'counter10times'
// }),
},
components: {
'b-child': bChild,
'c-child': cChild
},
methods: {
// 映射mutations到方法
...mapMutations({
modifyBName: 'b/changeBName'
}),
// 也可以这样实现
// ...mapMutations('b', {
// modifyBName: 'changeBName'
// }),
// 带有命名空间的子模块映射到组件的方法
...mapActions('a', {
modifyCName: 'callChangeCNameAsync'
}),
}
}
</script>
<style>
#app {
margin-top: 60px;
}
</style>
五、Vuex原理
1、说明
Vuex通过createStore创建了一个数据中心,然后通过发布-订阅模式来让订阅者监听到数据改变。
那么Vuex是怎么应用到Vue中的呢?
一个在Vue中使用Vuex的简单例子:
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import {createStore} from 'vuex';
const store = createStore({
state: {
message: 'hello'
},
mutations: {
change(state) {
state.message = 'world';
}
}
});
createApp(App).use(store).mount('#app');
export default {
name: 'App',
computed: {
info() {
return this.$store.state.message;
}
},
mounted() {
this.$store.commit('change');
}
}
可以看到,在Vue中使用Vuex,主要有3个关键步骤:
-
使用Vuex创建store,再将store注入Vue中。Vue组件中就可以通过this.$store来访问到store。
-
Vue使用computed获取$store中的状态。
-
Vue通过$store.commit和$store.dispatch来修改状态。
两个问题:
-
注入的原理是什么?为什么调用use()方法之后,就可以在组件通过$store来访问store了?
-
响应式原理是什么?为什么使用computed可以监听到store中的状态改变?
这两个是Vuex比较核心的两个原理。
2、注入原理
store注入 vue的实例组件的方式,是通过vue的 mixin机制,借助vue组件的生命周期钩子beforeCreate 完成的。
Vue.mixin({
beforeCreate() {
if (this.$options && this.$options.store) {
// 找到根组件 main 上面挂一个$store
this.$store = this.$options.store;
}
else {
// 非根组件指向其父组件的$store
this.$store = this.$parent && this.$parent.$store;
}
}
});
3、Vuex响应式原理
Vuex使用vue中的reactive
方法将state设置为响应式,原理和Vue组件的data设置为响应式是一样的。
// vuex/src/store-util.js
import {reactive} from 'vue';
store._state = reactive({
data: state
});
4、总结
Vuex是个状态管理器。
Vuex通过createStore创建了一个数据中心,然后通过发布-订阅模式来让订阅者监听到数据改变。
Vuex的store注入 vue的实例组件的方式,是通过vue的 mixin机制,借助vue组件的生命周期钩子beforeCreate 完成的。这样Vue组件就能通过this.$store获取到store了。
Vuex使用vue中的reactive
方法将state设置为响应式,这样组件就可以通过computed来监听状态的改变了。