构建 vuex(多组件数据共享)环境
基于脚手架创建项目,构建 vuex 多组件数据共享环境
以简易组件通信实例为例
效果是三个组件,共享一份数据,且:
- 任意一个组件都可以修改数据
- 三个组件的数据是同步的
创建一个空仓库
- 安装 vuex
安装 v3 版本,vue/vue-router/vuex 的版本对应一般是 2/3/3 或 3/4/4
- 新建 vuex 模块文件
新建store/index.js
专门存放 vuex
- 创建仓库
1 2 3
| Vue.use(Vuex);
new Vuex.Store();
|
- main.js 导入挂载
仓库导入挂载完毕后所有组件都能访问到它
核心概念- state 状态
提供数据
state
提供唯一的公共数据源,所有共享的数据都要统一放到Store
中的state
对象中存储
在state
对象中可以添加需要共享的数据
1 2 3 4 5 6 7 8 9 10
| const store = new Vuex.Store({ state: { count: 101, }, });
|
使用数据
- 通过 store 直接访问
获取store
: 1. this.$store 2. import 导入 store
模板中:{{ $store.state.xxx }}
组件逻辑中:this.$store.state.xxx
JS模块中:store.state.xxx
- 通过辅助函数(简化)
辅助函数- mapState
mapState
是辅助函数,帮助我们将store
中的数据自动映射到组件的计算属性中
若直接将state
中的数据定义在组件内的计算属性中:
1 2 3 4 5
| computed:{ count(){ return this.$store.state.count } }
|
这样写,虽然在使用时不那么繁琐,但若数据较多时再这样写就太麻烦了
而mapState
就可以简化这一过程
- 导入
mapState
1
| import { mapState } from 'vuex';
|
- 数组方式引入
state
- 展开运算符映射
使用展开运算符...
是为了不让 mapState 占据整个计算属性空间
数组中写要使用的共享的数据,以逗号隔开
1 2 3
| computed: { ...mapState(['count','title']) }
|
核心概念- mutations
严格模式
vuex 遵循单向数据流,组件中不能直接修改仓库中的数据
在实例中,要实现对数据count
的修改,若使用以下写法是不规范的
1 2
| this.$store.state.count++;
|
这样写一般不会报错,但是这样写是不规范的,要想给出错误提示,可以通过strict:true
开启严格模式
注意:严格模式,上线时需要移除,会消耗性能
1 2 3 4 5 6 7 8 9 10
| const store = new Vuex.Store({ strict: true, state: { count: 100, title: 'xxxx', }, });
|
mutations 修改数据
state
数据的修改只能通过mutations
- 仓库中定义
mutations
对象,对象中存放修改state
的方法
1 2 3 4 5 6 7 8 9 10 11 12
| const store = new Vuex.Store({ state: { count: 100, }, mutations: { addCount(state) { state.count += 1; }, }, });
|
- 组件中提交调用
mutations
1
| this.$store.commit('addCount');
|
mutations 传参
count
加一写一个方法,若是加二加三…也再单独写一个方法吗,可以但是不建议
对于同样的操作,只需要传递不同的参数即可
1
| this.$store.commit('方法名', 参数);
|
- 提供
mutation
函数(带参数-提交载荷 payload)
1 2 3 4 5 6
| mutations:{ addCount(state,n){ state.count+=n } }
|
- 页面中提交调用
mutation
1
| this.$store.commit('addCount', 10);
|
注意参数只能提交一个,若想要传递多个参数,可以包装成一个对象传递
eg:
1 2 3 4 5
| this.$store.commit('addCount', { count: 5, msg: '加五', });
|
练习- 实时输入,实时更新
- 输入框内容渲染
注意遵循单向数据流,不能使用v-model
绑定仓库的共享数据
使用:value
绑定数据
- 监听输入获取内容
@input
监听输入数据
1
| <input :value="count" @input="handleIpt" type="text" />
|
- 封装
mutation
处理函数
App.vue:
1 2 3 4 5 6 7 8
| methods: { handleIpt (e) { const newNum = +e.target.value this.$store.commit('changeCount', newNum) } }
|
- 调用传参
store/index.js:
1 2 3 4 5 6
| mutations: { changeCount (state, newCount) { state.count = newCount } }
|
辅助函数- mapMutations
mapMutations
和mapState
很像,语法也类似
它是把位于mutations
中的方法提取了出来,映射到组件methods
中
eg:
store/index.js:
1 2 3 4 5
| mutations: { subCount (state, num) { state.count -= num }, }
|
SonTwo.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import { mapMutations } from 'vuex' methods: { ...mapMutations(['subCount']), handleSub (num) { this.subCount(num) } }
methods:{ subCount(n){ this.$store.commit('subCount',n) } }
|
甚至于可以不需要写handleSub
事件处理函数,直接在按钮上绑定mutation
处理函数
这样的处理极大简化了写法
1 2 3
| <button @click="handleSub(5)">值-5</button>
<button @click="subCount(10)">值-10</button>
|
核心概念- actions
actions
处理异步操作
需求:一秒钟之后,修改state
的count
为 666
说明:mutations
必须是同步的(便于检测数据变化,记录调试)
- 提供
action
方法
1 2 3 4 5 6 7 8
| actions:{ setAsyncCount(context,num){ setTimeout(()=>{ context.commit('changeCount',num) },1000) } }
|
- 页面中
dispatch
调用
1
| this.$store.dispatch('setAsyncCount', 666);
|
eg:
store/index.js:
1 2 3 4 5 6 7 8 9 10 11
| actions: { setAsyncCount (context, num) { setTimeout(() => { context.commit('changeCount', num) }, 1000) } }
|
SonOne.vue:
1
| <button @click="changeAfterOneSec">一秒后count改为666</button>
|
1 2 3 4 5 6 7 8
| methods: { changeAfterOneSec () { this.$store.dispatch('setAsyncCount', 666) } }
|
当然,也可以手动传参
辅助函数- mapActions
mapActions
是把位于actions
中的方法提取了出来,映射到组件methods
中
例:将上面的setAsyncCount
方法映射到子组件一的methods
中
1 2 3 4 5 6 7 8 9 10 11 12
| import { mapActions } from 'vuex'; methods:{ ...mapActions(['setAsyncCount']), changeAfterOneSec (num) { this.setAsyncCount(num) } }
|
核心概念- getters
除了state
之外,有时还需要从state
中派生出一些状态,这些状态是依赖state
的(比如原本state
中有数量和价格,就可以派生出一个新的状态:总价),此时会用到getters
(类似于计算属性computed
)
例如:state
中定义了 list,为 1-10 的数组,组件中,需要显示所有大于 5 的数据
1 2 3
| state: { list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; }
|
定义getters
1 2 3 4 5 6 7 8
| getters: { filterList(state){ return state.list.filter(item=>item>5) } }
|
访问getters
- 通过
store
访问
1
| {{ $store.getters.filterList }}
|
- 通过辅助函数
辅助函数- mapGetters
将getters
方法映射到组件computed
中
1 2 3 4 5
| import { mapGetters } from 'vuex'
computed:{ ...mapGetters(['filterList']) }
|
核心概念- 模块 mudule(进阶语法)
模块的创建
由于vuex
使用单一状态树,应用的所有状态会集中到一个比较大的对象。
当应用变得非常复杂时,store
对象就有可能变得相当臃肿。(当项目变得越来越大时,vuex 会变得越来越难以维护)
模块拆分,以用户模块为例:
user 模块:store/modules/user.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const state = { userInfo: { name: 'andy', age: 18, }, }; const mutations = {}; const actions = {}; const getters = {}; export default { state, mutations, actions, getters, };
|
store/index.js:
1 2 3 4 5 6 7
| import user from './modules/user'; const store = new Vuex.Store({ modules: { user, }, });
|
如此一来就会发现 vuex 上会挂载好这个模块
尽管已经分模块了,但其实子模块的状态,还是会挂到根级别的state
中,属性名就是模块名
从本质上看还是单一状态树,但从可维护的角度看,子模块已被拆分到不同的文件中,较原来可维护性高得多
模块中 state 的访问
使用模块中 state
的数据:
- 直接通过模块名访问
1 2 3
| $store.state.模块名.xxx;
$store.state.user.userInfo.name;
|
- 通过
mapState
映射
- 默认根级别的映射:
mapState(['xxx'])
- 子模块的映射:
mapState('模块名',['xxx'])
-需要开启命名空间
1 2 3 4 5 6 7 8
| export default { namespaced: true, state, mutations, actions, getters, };
|
1
| mapState('user', ['userInfo']);
|
使用:
1
| <div>{{ userInfo }}</div>
|
模块中 getters 的访问
使用模块中getters
的数据:
- 直接通过模块名访问
1 2 3
| $store.getters['模块名/xxx'];
$store.state.user.userInfo.name;
|
- 通过
mapGetters
映射
- 默认根级别的映射:
mapGetters(['xxx'])
- 子模块的映射:
mapGetters('模块名',['xxx'])
-需要开启命名空间
模块中 mutation 的调用
注意:默认模块中的 mutation
和 actions
会被挂载到全局,需要开启命名空间,才会挂载到子模块
调用子模块中的 mutation
:
- 直接通过
store
调用:$store.commit('模块名/xxx',额外参数)
- 通过
mapMutations
映射
- 默认根级别的映射:
mapMutations(['xxx'])
- 子模块的映射:
mapMutations('模块名',['xxx'])
-需要开启命名空间
模块中 action 的调用(类比 mutation)
调用子模块中的 action
:
- 直接通过
store
调用:$store.dispatch('模块名/xxx',额外参数)
- 通过
mapActions
映射
- 默认根级别的映射:
mapActions(['xxx'])
- 子模块的映射:
mapActions('模块名',['xxx'])
-需要开启命名空间
源码
src/components/SonOne.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
| <template> <div class="sonbox"> <h3>Son1子组件</h3> <p> 从vuex中获取的值: <label>{{ count }}</label> </p> <button @click="handleAdd(1)">值+1</button> <button @click="handleAdd(5)">值+5</button> <button @click="changeTitle">改小标题</button> <button @click="changeAfterOneSec(666)">一秒后count改为666</button> <hr /> <div>{{ $store.getters.filterList }}</div> <hr /> <div>name:{{ $store.state.user.userInfo.name }}</div> <button @click="updateUser">更新个人信息</button> <button @click="updateUser2">一秒后更新信息</button>
<div>theme:{{ theme }}</div> <button @click="updateTheme">更新主题</button> <hr /> <div>{{ $store.getters['user/UpperCaseName'] }}</div> </div> </template> <script> import { mapState, mapActions } from 'vuex';
export default { created() { console.log(this.$store.getters); }, computed: { ...mapState(['count']), ...mapState('user', ['userInfo']), ...mapState('setting', ['theme', 'desc']), }, methods: { ...mapActions(['setAsyncCount']), handleAdd(num) { this.$store.commit('addCount', num); }, changeTitle() { this.$store.commit('changeTitle'); }, changeAfterOneSec(num) { this.setAsyncCount(num); }, updateUser() { this.$store.commit('user/setUser', { name: 'Matt', age: 20 }); }, updateUser2() { this.$store.dispatch('user/setUserSecond', { name: 'John', age: 28, }); }, updateTheme() { this.$store.commit('setting/setTheme', 'dark'); }, }, }; </script> <style lang="less" scoped> .sonbox { padding: 10px; margin: 10px 0; width: 80%; border: 1px solid #000; } </style>
|
src/components/SonTwo.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| <template> <div class="sonbox"> <h3>Son2子组件</h3> <p> 从vuex中获取的值: <label>{{ count }}</label> </p> <button @click="handleSub(1)">值-1</button> <button @click="handleSub(5)">值-5</button> <button @click="subCount(10)">值-10</button> <button @click="changeTitle('SonTwo组件修改的标题')">改标题</button> <hr /> <div>{{ filterList }}</div> <hr /> <div>{{ user.userInfo.name }}</div> <div>{{ setting.theme }}</div> <div>user模块的数据:{{ userInfo }}</div> <button @click="setUser({name:'lily',age:17})">更新个人信息</button> <button @click="setUserSecond({name:'Tommy',age:15})">一秒后更新个人信息</button> <div>setting模块的数据:{{ theme }}-{{ desc }}</div> <button @click="setTheme('pink')">更新主题</button> <hr /> <div>{{ UpperCaseName }}</div> </div> </template> <script> import { mapState, mapMutations, mapGetters, mapActions } from 'vuex';
export default { computed: { ...mapState(['count', 'user', 'setting']), ...mapState('user', ['userInfo']), ...mapState('setting', ['theme', 'desc']), ...mapGetters(['filterList']), ...mapGetters('user', ['UpperCaseName']), }, methods: { ...mapMutations(['subCount', 'changeTitle']),
...mapMutations('setting', ['setTheme']), ...mapMutations('user', ['setUser']), ...mapActions('user', ['setUserSecond']), handleSub(num) { this.subCount(num); }, }, }; </script> <style lang="less" scoped> .sonbox { padding: 10px; margin: 10px 0; width: 80%; border: 1px solid #000; } </style>
|
src/router/index.js:
1 2 3 4 5 6 7 8 9 10 11 12
| import Vue from 'vue'; import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [];
const router = new VueRouter({ routes, });
export default router;
|
src/store/index.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| import Vue from 'vue'; import Vuex from 'vuex'; import user from './modules/user'; import setting from './modules/setting';
Vue.use(Vuex);
const store = new Vuex.Store({ strict: true, state: { count: 100, title: '小标题', list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], }, mutations: { addCount(state, num) { state.count += num; }, subCount(state, num) { state.count -= num; }, changeTitle(state, newTitle) { state.title = newTitle || '更改后的小标题'; }, changeCount(state, newCount) { state.count = newCount; }, }, actions: { setAsyncCount(context, num) { setTimeout(() => { context.commit('changeCount', num); }, 1000); }, }, getters: { filterList(state) { return state.list.filter(item => item > 5); }, }, modules: { user, setting, }, });
export default store;
|
src/store/module/setting.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const state = { theme: 'light', desc: '测试demo', }; const mutations = { setTheme(state, newTheme) { state.theme = newTheme; }, }; const actions = {}; const getters = {}; export default { namespaced: true, state, mutations, actions, getters, };
|
src/store/module/user.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| const state = { userInfo: { name: 'andy', age: 18, }, score: 66, }; const mutations = { setUser(state, newUserInfo) { state.userInfo = newUserInfo; }, }; const actions = { setUserSecond(context, newUserInfo) { setTimeout(() => { context.commit('setUser', newUserInfo); }, 1000); }, }; const getters = { UpperCaseName(state) { return state.userInfo.name.toUpperCase(); }, }; export default { namespaced: true, state, mutations, actions, getters, };
|
src/App.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| <template> <div class="main"> <h2>根组件{{ count }}</h2> <h3>{{ title }}</h3> <input :value="count" @input="handleIpt" type="text" /> <SonOne></SonOne> <SonTwo></SonTwo> </div> </template> <script> import SonOne from './components/SonOne.vue'; import SonTwo from './components/SonTwo.vue'; import { mapState } from 'vuex'; export default { data() { return {}; }, components: { SonOne, SonTwo, }, created() { console.log(this.$store); console.log(this.$store.state.count); }, computed: { ...mapState(['count', 'title']), }, methods: { handleIpt(e) { const newNum = +e.target.value; this.$store.commit('changeCount', newNum); }, }, }; </script> <style lang="less"> .main { padding: 10px; border: 1px solid #000; } </style>
|
src/main.js:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import Vue from 'vue'; import App from './App.vue'; import router from './router'; import store from '@/store/index'; console.log(store.state.count);
Vue.config.productionTip = false;
new Vue({ router, store, render: h => h(App), }).$mount('#app');
|