目录
一、概念
vue2 契合 vuex | vue3 契合 pinia
1. 状态管理
在开发中,应用程序需要处理各种各样的数据,这些数据需要保存在应用程序中的某一个位置,对于这些数据的管理就称之为是状态管理
- 在Vue开发中,使用组件化的开发方式
- 而在组件中定义data或者在setup中返回使用的数据,这些数
据称之为state - 在模块template中可以使用这些数据,模块最终会被渲染成
DOM,称之为View - 在模块中会产生一些行为事件,处理这些行为事件时,有可能
会修改state,这些行为事件称之为actions
2. Vuex的状态管理
- 可以考虑将组件的内部状态抽离出来,以一个全局单例的方式来管理
- 在这种模式下,组件树构成了一个巨大的 “试图View”
- 不管在树的哪个位置,任何组件都能获取状态或者触发行为
- 通过定义和隔离状态管理中的各个概念,并通过强制性的规则来维护视图和状态间的独立性,代码会变得更加结构化和易于维护、跟踪
3. 单一状态树
Vuex 使用单一状态树 :
- 用一个对象就包含了全部的应用层级的状态
- 采用的是SSOT,Single Source of Truth,也可以翻译成单一数据源
- 每个应用将仅仅包含一个 store 实例
- 单状态树和模块化并不冲突,可以使用module模块
单一状态树的优势 :
- 如果状态信息是保存到多个Store对象中的,那么之后的管理和维护等等都会变得特别困难
- 所以Vuex使用了单一状态树来管理应用层级的全部状态
- 单一状态树能够让我们最直接的方式找到某个状态的片段
- 而且在之后的维护和调试过程中,也可以非常方便的管理和维护
单一状态树的缺点 : 不够灵活
二、Vuex 的基本使用
1. Vuex的安装
npm install vuex
2. 创建store
每一个Vuex应用的核心就是store(仓库)
store本质上是一个容器,它包含着你的应用中大部分的状态(state)
Vuex和单纯的全局对象的区别 :
- 第一:Vuex的状态存储是响应式的
- 当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会被更新
- 第二:不能直接改变store中的状态
- 改变store中的状态的唯一途径就显示提交 (commit) mutation
- 这样使得我们可以方便的跟踪每一个状态的变化,从而让我们能够通过一些工具帮助我们更好的管理应用的状态
3. 在store文件夹下创建index.js
// 1. 引入
import { createStore } from 'vuex';
// 2. 创建store对象
const store = createStore({
// 3. 定义state
state: () => ({
// 4. 定义数据
count: 100
}),
// 5. 定义mutations
mutations: {
// 6. 定义方法
increment(state) {
state.count++;
}
}
});
// 5. 导出
export default store;
4. 在main.js中引入
import { createApp } from 'vue';
import App from './App.vue';
// 1. 导入
import store from './store';
// 2. 使用
createApp(App).use(store).mount('#app');
5. 组件中使用sotre
<template>
<div class="app">App 页面</div>
<!-- 1. 模版中使用 -->
<h2>模版 : {{ $store.state.count }}</h2>
<h2>computed : {{ storeCount }}</h2>
<h2>解构count : {{ count }}</h2>
<button @click="btnClick">增加</button>
</template>
<script setup>
// 2. 在js中使用
import { useStore } from 'vuex';
import { computed, toRefs } from 'vue';
const store = useStore();
console.log(store.state.count);
// 01 - computed包裹一下变成响应式
const storeCount = computed(() => store.state.count);
// 02 - 解构成ref对象
const { count } = toRefs(store.state);
// 监听按钮的点击
const btnClick = () => {
// 触发mutation,让count++
store.commit('increment');
};
</script>
6. 效果
三、核心概念 State
1. 直接使用
<template>
<div class="app">App 页面</div>
<!-- 1. 模版中使用 -->
<h2>模版 : {{ $store.state.count }}</h2>
<h2>computed : {{ storeCount }}</h2>
<h2>解构count : {{ count }}</h2>
</template>
<script setup>
// 2. 在js中使用
import { useStore } from 'vuex';
import { computed, toRefs } from 'vue';
const store = useStore();
console.log(store.state.count);
// 01 - computed包裹一下变成响应式
const storeCount = computed(() => store.state.count);
// 02 - 解构成ref对象
const { count } = toRefs(store.state);
</script>
2. mapState
使用
<template>
<div class="app">App 页面</div>
<h2>模版 : {{ $store.state.count }}</h2>
<h2>mapState : {{ cCount }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore, mapState } from 'vuex';
import { computed } from 'vue';
// 1. 拿到store对象
const store = useStore();
// 2. 使用mapState解构出方法
// const { count } = mapState(['count']);
// 数组和对象都可以
const { count } = mapState({
count: (state) => state.count
});
// 3. 拿到对应的值
/**
* count => 底层还是通过this.$store.state.count去拿的
* setup 中没有this,所以传递过去时需要显示绑定一个$store
*
* 通过显示绑定bind对象,让this指向这个对象
* 然后底层就能拿到this.$store了
*/
const cCount = computed(count.bind({ $store: store }));
const btnClick = () => {
store.commit('increment');
};
</script>
封装 useState.js 文件
代码
import { useStore, mapState } from 'vuex';
import { computed } from 'vue';
export default function useState(mapper) {
// 1. 拿到store对象
const store = useStore();
// 2. 使用mapState拿到对应的方法
const stateFnsObj = mapState(mapper);
// 4. 定一个接受对象
const newState = {};
// 5.遍历
Object.keys(stateFnsObj).forEach((key) => {
// 6. 生成绑定过的对象
newState[key] = computed(stateFnsObj[key].bind({ $store: store }));
});
// 7. 返回
return newState;
}
使用
<template>
<div class="app">App 页面</div>
<h2>模版 : {{ $store.state.count }}</h2>
<!-- 3. 展示 -->
<h2>封装哒 : {{ fCount }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore } from 'vuex';
// 1. 导入封装的方法
import useState from './hooks/useState';
// 2. 使用
const { count: fCount } = useState(['count']);
const store = useStore();
const btnClick = () => {
store.commit('increment');
};
</script>
四、核心概念 Getters
1. 基本使用
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
increment(state) {
state.count++;
}
},
// 定义getters
getters: {
doubleCount(state) {
return state.count * 2;
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>模版 : {{ $store.state.count }}</h2>
<!-- 使用getters -->
<h2>doubleCount : {{ $store.getters.doubleCount }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore } from 'vuex';
const store = useStore();
const btnClick = () => {
store.commit('increment');
};
</script>
2. getters第二个参数
getters可以接收第二个参数,用来调用其他的getters
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
increment(state) {
state.count++;
}
},
getters: {
doubleCount(state, getters) {
// 可以调用其他的getters函数
return state.count * 2 + getters.mathRanDom;
},
// 生成[5,50)的随机数
mathRanDom() {
return Math.floor(Math.random() * 45) + 5;
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>模版 : {{ $store.state.count }}</h2>
<h2>random : {{ $store.getters.mathRanDom }}</h2>
<!-- 使用getters -->
<h2>doubleCount : {{ $store.getters.doubleCount }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore } from 'vuex';
const store = useStore();
const btnClick = () => {
store.commit('increment');
};
</script>
3. getters的返回函数
getters中的函数本身,可以返回一个函数,可用来接受参数
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
increment(state) {
state.count++;
}
},
getters: {
doubleCount(state, getters) {
// 可以调用其他的getters函数
return state.count * 2 + getters.mathRanDom;
},
// 生成[5,50)的随机数
mathRanDom() {
return Math.floor(Math.random() * 45) + 5;
},
countAddNum(state) {
// 可用来接受传进来的参数
return function (num) {
return state.count + num;
};
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>模版 : {{ $store.state.count }}</h2>
<!-- 基本使用getters -->
<h2>doubleCount : {{ $store.getters.doubleCount }}</h2>
<!-- 随机数 -->
<h2>random : {{ $store.getters.mathRanDom }}</h2>
<!-- 传递参数 -->
<h3>countAddNum : {{ $store.getters.countAddNum(10) }}</h3>
<h3>countAddNum : {{ $store.getters.countAddNum(20) }}</h3>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore } from 'vuex';
const store = useStore();
const btnClick = () => {
store.commit('increment');
};
</script>
4. mapGetters
使用
<template>
<div class="app">App 页面</div>
<h2>{{ doubleCount }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { computed, toRefs, watch, watchEffect } from 'vue';
import { useStore, mapGetters } from 'vuex';
const store = useStore();
// 1. 第一种方法
// const { doubleCount } = toRefs(store.getters);
// 2. 第二种方法
// const doubleCount = computed(() => store.getters.doubleCount);
// 3. 第三种方法,使用mapGetters
const { doubleCount: doubleCountFn } = mapGetters(['doubleCount']);
const doubleCount = computed(doubleCountFn.bind({ $store: store }));
// 这样时时打印数据
watchEffect(() => {
console.log(doubleCount.value);
});
const btnClick = () => {
store.commit('increment');
};
</script>
封装 useGetters.js 文件
代码
import { useStore, mapGetters } from 'vuex';
import { computed } from 'vue';
export default function useGetters(mapper) {
const store = useStore();
// 1. 接受全部getters
const gettersFn = mapGetters(mapper);
// 2. 定义新的函数数组
const newGetters = {};
// 3. 遍历绑定
Object.keys(gettersFn).forEach((key) => {
newGetters[key] = computed(gettersFn[key].bind({ $store: store }));
});
// 返沪
return newGetters;
}
使用
<template>
<div class="app">App 页面</div>
<h2>{{ doubleCount }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { watchEffect } from 'vue';
import { useStore } from 'vuex';
// 1. 导入
import useGetters from '@/hooks/useGetters';
// 2. 使用
const { doubleCount } = useGetters(['doubleCount']);
const store = useStore();
const btnClick = () => {
store.commit('increment');
};
// 这样时时打印数据
watchEffect(() => {
console.log(doubleCount.value);
});
</script>
五、核心概念 Mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
1. 重要原则
mutation 必须是同步函数
- 因为devtool工具会记录mutation的日记
- 每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照
- 但是在mutation中执行异步操作,就无法追踪到数据的变化
2. 基本使用
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
increment(state) {
state.count++;
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore } from 'vuex';
const store = useStore();
const btnClick = () => {
// 使用mutation中定义的方法
store.commit('increment');
};
</script>
3. 携带数据
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
// 接受传递过来的参数
increment(state, num) {
state.count += num;
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore } from 'vuex';
const store = useStore();
const btnClick = () => {
// 传递数据
store.commit('increment', 10);
};
</script>
4. 常量类型
定义mutation_types.js
// 定义成常量类型
export const INCREMENT = 'increment';
代码
import { createStore } from 'vuex';
// 1. 导入
import { INCREMENT } from './mutation_types';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
// 2. 使用常量作为名称,使用计算属性值
[INCREMENT](state, num) {
state.count += num;
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore } from 'vuex';
// 1. 导入
import { INCREMENT } from '@/store/mutation_types';
const store = useStore();
const btnClick = () => {
// 2. 使用
store.commit(INCREMENT, 10);
};
</script>
5. mapMutations
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { useStore, mapMutations } from 'vuex';
import { INCREMENT } from '@/store/mutation_types';
const store = useStore();
// 1. 接受全部函数
const mutationFns = mapMutations([INCREMENT]);
// 2. 定义新的函数数组
const newMutations = {};
// 3. 遍历绑定
Object.keys(mutationFns).forEach((key) => {
newMutations[key] = mutationFns[key].bind({ $store: store });
});
// 4. 从新生成的函数数组中解构出来
const { increment } = newMutations;
const btnClick = () => {
// 5. 使用
increment(10);
};
</script>
封装 useMutations.js 文件
代码
import { useStore, mapMutations } from 'vuex';
export default function useMutations(mapper) {
const store = useStore();
// 1. 接受全部函数
const mutationFns = mapMutations(mapper);
// 2. 定义新的函数数组
const newMutations = {};
// 3. 遍历绑定
Object.keys(mutationFns).forEach((key) => {
newMutations[key] = mutationFns[key].bind({ $store: store });
});
// 返回对象
return newMutations;
}
使用
<script setup>
import { useStore, mapMutations } from 'vuex';
// 1. 导入
import useMutations from '@/hooks/useMutations';
// 2. 解构
const { increment } = useMutations(['increment']);
const btnClick = () => {
// 3. 使用
increment(10);
};
</script>
六、核心概念 Actions
Action类似于mutation
- Action提交的是mutation,而不是直接变更状态
- Action可以包含任意异步操作
参数context :
- context是一个和store实例均有相同方法和属性的context对象
- 所以可以从其中获取到commit方法来提交一个mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
- 但context不是store对象
1. 基本使用
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
increment(state) {
state.count++;
}
},
getters: {
doubleCount(state, getters) {
return state.count * 2 + getters.mathRanDom;
},
mathRanDom() {
return Math.floor(Math.random() * 45) + 5;
}
},
actions: {
// 定义actions方法
incrementAction(context) {
// 1. 可进行接口请求
// balabalabala~
// 2. 通过mutation来修改state的状态
context.commit('increment');
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { watchEffect } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const btnClick = () => {
// 调用action方法
store.dispatch('incrementAction');
};
</script>
2. 传递参数
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
increment(state, num = 10) {
state.count += num;
}
},
getters: {
doubleCount(state, getters) {
return state.count * 2 + getters.mathRanDom;
},
mathRanDom() {
return Math.floor(Math.random() * 45) + 5;
}
},
actions: {
// 定义actions方法,接受参数
incrementAction(context, num) {
// 调用mutations方法
context.commit('increment', num);
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { watchEffect } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const btnClick = () => {
// 调用action方法,传递参数
store.dispatch('incrementAction', 9);
};
</script>
3. mapActions
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { watchEffect } from 'vue';
import { useStore, mapActions } from 'vuex';
const store = useStore();
// 1. 接受全部的actions
const actionsFn = mapActions(['incrementAction']);
// 2. 定义新的actions对象
const newActions = {};
// 3. 遍历
Object.keys(actionsFn).forEach((key) => {
newActions[key] = actionsFn[key].bind({ $store: store });
});
// 4. 解构
const { incrementAction } = newActions;
const btnClick = () => {
// 5. 使用
incrementAction(9);
};
</script>
封装 useActions.js 方法
代码
import { useStore, mapActions } from 'vuex';
export default function useActions(mapper) {
const store = useStore();
// 1. 接受全部的actions
const actionsFn = mapActions(['incrementAction']);
// 2. 定义新的actions对象
const newActions = {};
// 3. 遍历
Object.keys(actionsFn).forEach((key) => {
newActions[key] = actionsFn[key].bind({ $store: store });
});
return newActions;
}
使用
<template>
<div class="app">App 页面</div>
<h2>{{ $store.state.count }}</h2>
<button @click="btnClick">改变</button>
</template>
<script setup>
import { watchEffect } from 'vue';
import { useStore, mapActions } from 'vuex';
// 1. 导入
import useActions from '@/hooks/useActions';
// 2. 解构
const { incrementAction } = useActions(['incrementAction']);
const btnClick = () => {
// 5. 使用
incrementAction(9);
};
</script>
4. 异步操作
代码
import { createStore } from 'vuex';
const store = createStore({
state: () => ({
// 1. 保存请求来的数据
arrayList: []
}),
mutations: {
// 2. 保存数据到 arrayList 中
saveArrayList(state, list) {
state.arrayList = list;
}
},
actions: {
// 3. 定义异步请求actions
async fetchDataList(context) {
/**
* 1. 请求
*/
// fetch('http://xxxx').then((res) => {
// res.json().then((data) => {
// console.log(data);
// context.commit('saveArrayList', data.list);
// });
// });
/**
* 2. 优化,Promise链式调用
*/
// fetch('http://xxxx')
// .then((res) => {
// return res.json();
// })
// .then((data) => {
// console.log(data);
// context.commit('saveArrayList', data.list);
// });
/**
* 3. 使用async await
*/
// const res = await fetch('http://xxxx');
// const data = await res.json();
// context.commit('saveArrayList', data.list);
/**
* 4. 模拟,并返回一个promise
*/
return new Promise((resolve) => {
setTimeout(() => {
const data = [1, 2, 3, 4, 5];
// 保存在state中
context.commit('saveArrayList', data);
// 把数据返回出去
resolve(data);
}, 1000);
});
}
}
});
export default store;
使用
<template>
<div class="app">App 页面</div>
<!-- 3. 显示 -->
<h2>{{ dataList }}</h2>
<!-- 4. 也可以直接显示 -->
<h2>{{ $store.state.arrayList }}</h2>
<h2></h2>
</template>
<script setup>
import { ref } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const dataList = ref([]);
// 1. 调用dispatch方法,请求数据
store.dispatch('fetchDataList').then((res) => {
// 2. 拿到数据
dataList.value = res;
console.log(res);
});
</script>
七、核心概念 Module
Module :
- 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得相当臃肿
- 为了解决以上问题,Vuex 可以将 store 分割成模块(module)
- 每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
1. 局部状态
state : 使用的时候需要在前面加上模块的名称
mutation、getters、action : 默认集成在顶层中,可直接使用
ps : 所以,state可以定义相同的名字,但是其他的重名会报错,命名冲突问题
01 - 定义模块A.js
export default {
// 模块数据
state: () => ({
m_Age: 18
}),
// 模块方法
mutations: {
// state => 指的是本模块的数据
m_AddAge(state) {
state.m_Age++;
}
},
// 模块action
actions: {
/**
* state => 指的是本模块的数据
* commit => 可调用所有方法
* rootState => 指的是根模块的数据
*/
m_AddAgeAction({ state, commit, rootState }) {
// 调用本模块方法
commit('m_AddAge');
// 调用根模块方法
commit('increment');
}
},
// 模块action
getters: {
/**
* state => 指的是本模块的数据
* getters => 可调用所有getters
* rootState => 指的是根模块的数据
*/
getMAge(state, getters, rootState) {
return state.m_Age;
}
}
};
02 - 定义store.js
import { createStore } from 'vuex';
// 1.导入模块
import moduleA from './moduleA';
const store = createStore({
state: () => ({
count: 100
}),
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAction({ commit }) {
commit('increment');
}
},
getters: {
getCount(state) {
return state.count;
}
},
// 2. 注册一下
modules: {
moduleA
}
});
export default store;
03 - 使用
<template>
<div class="app">App 页面</div>
<h2>根目录 :</h2>
count : {{ $store.state.count }}<br />
count : {{ $store.getters.getCount }}
<hr />
<hr />
<hr />
<h2>模块目录 :</h2>
m_Age : {{ $store.state.moduleA.m_Age }} <br />
m_Age : {{ $store.getters.getMAge }}
</template>
<script setup>
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
/**
* 调用根模块
*/
// state
const count = computed(() => store.state.count);
console.log('count', count.value);
// getters
const getCount = computed(() => store.getters.getCount);
console.log('getMAge', getCount.value);
// mutation
store.commit('increment');
// action
store.dispatch('incrementAction');
/**
* 调用模块内
*/
// state
const m_Age = computed(() => store.state.moduleA.m_Age);
console.log('m_Age', m_Age.value);
// getters
const getMAge = computed(() => store.getters.getMAge);
console.log('getMAge', getMAge.value);
// mutation
store.commit('m_AddAge');
// action
store.dispatch('m_AddAgeAction');
</script>
2. 命名空间
默认情况下,模块内部的action、mutation、getters仍然是注册在全局的命名空间中的 :
- 这样使得多个模块能够对同一个 action 或 mutation 作出响应
- Getter 同样也默认注册在全局命名空间
如果希望模块具有更高的封装度和复用性
- 可以添加 namespaced: true 的方式使其成为带命名空间的模块
- 当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
01 - 基本使用
定义模块A.js
export default {
// 加上这个属性
namespaced: true,
state: () => ({
m_Age: 18
}),
......
};
使用
<template>
<div class="app">App 页面</div>
<h2>根目录 :</h2>
count : {{ $store.state.count }}<br />
count : {{ $store.getters.getCount }}
<hr />
<hr />
<hr />
<h2>模块目录 :</h2>
m_Age : {{ $store.state.moduleA.m_Age }} <br />
m_Age : {{ $store.getters['moduleA/getMAge'] }}
</template>
<script setup>
import { ref, computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
/**
* 调用根模块
*/
// state
const count = computed(() => store.state.count);
console.log('count', count.value);
// getters
const getCount = computed(() => store.getters.getCount);
console.log('getMAge', getCount.value);
// mutation
store.commit('increment');
// action
store.dispatch('incrementAction');
/**
* 调用模块内
*/
// state
const m_Age = computed(() => store.state.moduleA.m_Age);
console.log('m_Age', m_Age.value);
// getters
const getMAge = computed(() => store.getters['moduleA/getMAge']);
console.log('getMAge', getMAge.value);
// mutation
store.commit('moduleA/m_AddAge');
// action
store.dispatch('moduleA/m_AddAgeAction');
</script>
02 - module修改或派发根组件
如果希望在action中修改root中的state