vuex 梳理
什么是vuex
vuex 是为vue 应用程序开发的状态管理模式,简单的理解就是公共变量管理工具,用于组件间通信
vuex解决了什么问题
- 多个组件依赖于同一状态时,对于多层嵌套的组件的传参将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
- 来自不同组件的行为需要变更同一状态。以往采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
什么时候用到vuex
当项目遇到以下两种场景时
- 多个组件依赖于同一状态时
- 来自不同组件的行为需要变更同一状态
引入vuex
安装依赖
npm install vuex --save
在项目目录src中建立 store文件夹
在store文件夹下新建index.js文件
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
//不是在生产环境debug为true
const debug = process.env.NODE_ENV !== 'production' ? true : false;
//创建Vuex实例对象
const store = new Vuex.Store({
strict: debug, // 在不是生产环境下都开启严格模式(这个视情况而定要不要加上)
state: {
},
getters: {
},
actions: {
}
mutations: {
}
})
export default store;
然后再main.js中引入Vuex
import Vue from 'vue';
import App from './App.vue';
import store from './store';
new Vue({
el: "#app",
store,
render: h => h(App)
});
核心五大属性
state 单一状态树
定义
vuex使用单一状态数,即用一个对象包含全部的状态数据。
state
所为构造器选项,定义了所有我们需要的基本状态参数
// 可抽离变量(也可抽成文件)
const state = {
count: 0
};
//加载到 Vuex上
export default new Vuex.Store({
state
})
调用
- 这里的调用只有阅读功能,没有修改功能。修改state的值只能通过mutations进行修改
-
直接调用
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } } }
-
或者使用辅助函数mapState
import { mapState } from 'vuex' export default { // ... // 写法1 computed: mapState({ // 1.1箭头函数可使代码更简练 count: state => state.count, // 1.2传字符串参数 'count' 等同于 `state => state.count` countAlias: 'count', // 1.3为了能够使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state) { return state.count + this.localCount } }) } //写法2 简单点写法,这种就没法再写其它的非vuex类型的计算属性了 computed: mapState([ // 映射 this.count 为 store.state.count 'count' ]) //写法3 和其他计算属性混用,一般用这种 computed: { localComputed () { /* ... */ }, // 使用对象展开运算符将此对象混入到外部对象中 ...mapState({ // ... }) }
-
input 如何使用 v-model动态修改state中的值
<input v-model="message"> computed: { message: { get () { return this.$store.state.message }, set (value) { this.$store.commit('updateMessage', value) } } }
getter 计算属性
有时候我们需要从store的state中派生出一些状态,比如求列表length
- @param 接受两个传参 第一个是state,第二个是getters(可以用来访问其他的getter)
定义
const getters = {
// 极简写法,这里的值必须是 函数,函数返回值即为键的值
total: state => state.price * state.number,
// 主文件监听user模块下的数据
token: state => state.user.token,
// 加入监听其他getters
discountTotal: (state, getters) => state.discount * getters.total,
};
//加载到 Vuex上
export default new Vuex.Store({
state,
getters
})
调用
-
直接调用
computed: { total() { return this.$store.getters.total }, discountTotal() { return this.$store.getters.discountTotal } }
-
使用 mapGetters
使用方式和mapState相同,不重复说明
mutation 修改state的值
函数定义规则:大写字母+下划线 例:SET_INFO
注意
:这里的函数都是同步的,不可定义异步的,比如:接口调用,Promise,async都不可用。会报错。如果需要写异步函数,请到atcion中定义官方给的解释:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的
定义
mutation 接受两个传参第一个是state,第二个是payload 负载(传入信息)
const mutations = {
/**
* 存储用户信息
* @param state state 中存储的数据
* @param info payload 负载(传入信息)
*/
SET_INFO: (state, info) => {
state.info = info;
}
};
//加载到 Vuex上
export default new Vuex.Store({
state,
getters,
mutations
})
调用
-
直接调用
commit()可以接受除state外的额外的参数,该参数成为mutation的一个载荷,载荷一般是包含多个字段的对象
this.$store.commit('SET_INFO',{ name: "dpw", id: "123456", token: token })
-
使用辅助函数 mapMutations
import { mapMutations } from 'vuex' export default { // ... created() { // 直接调用 this.SET_INFO({name: "dpw",id: "123456",token: token}) // 别名方法调用 this.setInfo({name: "dpw",id: "123456",token: token}) }, methods: { ...mapMutations([ // 直接取方法 'SET_INFO', ...mapMutations({ // 定义别名 setInfo: 'SET_INFO' }) } }
action 处理异步事件
- 命名方式:首字母大写的驼峰命名法:例子:ChangeTheme
- action 提交的是mutation,而不是直接变更状态。(*不可直接变更)
- aciton 可以包含任意异步操作。
- 使用sotre.dispatch()分发Actions.
- action同样支持载荷方式个对象方式进行分发
分析
- aciton 和mutation的区别
-
aciton提交的是mutation,而不是直接变更状态。
-
aciton可以包含任意异步操作。mutation只能是同步操作。
-
提交方式不同,aciton用
this.$store.dispatch('GetUserInfo',data)
来提交。mutation是用this.$store.commit('SET_INFO', data)
来提交。 -
接受参数不同
传参分析
aciton接受两个参数,第一个是context对象(可以解构使用相应属性),第二个是传入负载
context包含六个种属性{ state, // 在模块中视为局部变量(当前模块) rootState, //全局变量 只能用于模块中 commit, //**核心调用mutation中方法,因为在aciton中不可直接修改state的值;当前模块和其他模块 dispatch, //调用aciton中异步方法;当前模块和其他模块 getters, //视为局部计算属性(当前模块) rootGetters //全局计算属性 只能用于模块中 }
-
定义
// 导入获取用户信息接口 (单个接口导入)
import { getUserInfo } from "@/interface/user";
/* // 整体导入
// 调用 getUserInfo :userAPI.getUserInfo().then()
import userAPI from "@/interface/user"; */
// 定义同步方法
const mutations = {
/**
* 存储用户信息
* @param state state 中存储的数据
* @param theme payload 负载(传入信息)
*/
SET_THEME: (state, theme) => {
state.theme = theme;
}
};
// 定义异步方法
const actions = {
// 改变用户主题
// 写法1 async await 写法
async getUserInfo({commit,state}) {
let res = (await axios.get('/api/security/current')).data;
commit(GET_USERINFO, res)
},
// 写法2 Promise 异步
ActionA({commit}){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
commit('SET_THEME')
resolve()
},1000)
})
}
//写法3 (常用)因为axois本身就是基于Promise的封装,那我们调用接口时可省略
ChangeTheme({ commit, state }, loginForm) {
getUserInfo(loginForm).tnen(res =>{
// 调用mutation中的方法
commit("SET_THEME", theme);
// 调用action的其他异步方法
this.dispatch("ActionA")
})
// catch错误咱们基线已经做了异常同意捕获了,没有特殊情况不用处理 catch回调,只用处理then回调就可以了
}
};
//加载到 Vuex上
export default new Vuex.Store({
state,
getters,
mutations,
actions
})
调用
-
直接调用
this.$store.dispatch('GetUserInfo', this.ruleForm)
-
使用辅助函数 mapActions
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
module 模块化(项目中一般用法)
原因:因为使用单一状态数,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就会变得十分臃肿,所以将store分割成模块。每个模块拥有自己的state,mutations,actions,getters
使用模块化必须开启命名空间 namespaced:true
默认情况下,模块内部的action、mutation和getter是注册在全局命名空间,如果多个模块中action、mutation的命名是一样的,那么提交mutation、action时,将会触发所有模块中命名相同的mutation、action。
这样有太多的耦合,如果要使你的模块具有更高的封装度和复用性,你可以通过添加
namespaced: true
的方式使其成为带命名空间的模块。带命名空间的模块提交全局变量
将{root: true}作为第三个参数传给dispatch 或 commit
this.$store.dispatch('actionA', null, { root: true }) this.$store.commit('mutationA', null, { root: true })
组件中获取带有命名空间的moduleA中的state数据
//方法1 基本方式 this.$store.state.moduleA.count //方法2 mapState辅助函数方式 computed: mapState({ count:state => state.moduleB.countB }) // 一般用这种,因为一般还要写其它的计算属性 computed: { ...mapState("moduleC", ["countC"]) }
组件中调用命名空间中的getters
// 方法1 基本方式 this.$store.getters['moduleA/moduleAGetter'] // 方法2 mapGetters辅助函数方式 // 此处的moduleA,不是以前缀的形式出现!!,是模块命名空间 computed: { ...mapGetters('moduleA',['moduleAGetter']), } // 方法3 别名状态在 computed: { ...mapGetters({ paramGetter:'moduleA/moduleAGetter' }
组件中调用模块中的mutations
// 方法1 基础用法 this.$store.commit('moduleA/moduleAMutation'); // 方法2 mapMutations辅助函数 ...mapMutations('moduleA',['moduleAMutation']) // 方法3 别名状态下 ...mapMutations({ changeNameMutation:'moduleA/moduleAMutation' })
组件中调用模块中的actions
// 方法1 基础用法 this.$store.dispatch('moduleA/moduleAAction'); // 方法2 mapActions辅助函数 methods: { ...mapActions('moduleA',['moduleAAction']) } // 方法3 别名状态下 methods: { ...mapActions({ changeNameAction:'moduleA/moduleAAction' }) }
使用mapSate, mapGetters,mapActions和mapMutations这些函数来绑定命名空间模块
使用
createNamespacedHelpers
创建基于某个命名空间的辅助函数
import { createNamespacedHelpers } from 'vuex'; const { mapState, mapActions } = createNamespacedHelpers('moduleA'); export default { computed: { // 在 `module/moduleA` 中查找 ...mapState({ a: state => state.a, b: state => state.b }) }, methods: { // 在 `module/moduleA` 中查找 ...mapActions([ 'actionA', 'actionB' ]) } }
引入
-
在store文件夹中新建module模块文件夹
-
在module文件夹中新建moduleA.js和moduleB.js文件。在文件中写入
const state={ //... } const getters={ //... } const mutations={ //... } const actions={ //... } export default{ namespaced: true, //开启命名空间 state, getters, mutations, actions }
3.然后再 index.js引入模块
方法1 :逐个引入
import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); import moduleA from './module/moduleA' import moduleB from './module/moduleB' const store = new Vuex.Store({ modules:{ moduleA, moduleB } }) export default store
方法2:统一挂载(基线采用方法)
使用vue 的webpack属性将文件统一挂载
只需要将自己的子模块处理好,不用管挂载的问题
import Vue from "vue"; import Vuex from "vuex"; import getters from "./getters"; Vue.use(Vuex); // 统一导入:三个参数 文件路径, 是否向下查询,文件后缀名正则 const modulesFiles = require.context("./modules", true, /\.js$/); //使用reduce函数挂载 // Array.prototype.reduce() reduce是Array数组构造器上挂载的方法 /* reducer 函数接收4个参数: Accumulator (acc) (累计器) Current Value (cur) (当前值) Current Index (idx) (当前索引) Source Array (src) (源数组) */ /* 返回值 函数累计处理的结果 */ const modules = modulesFiles.keys().reduce((modules, modulePath) => { // 获取文件名 const moduleName = modulePath.replace(/^\.\/(.*)\.\w+$/, "$1"); // 获取文件 const value = modulesFiles(modulePath); // 将模块导入到 modules中=》{ moduleA, moduleB} modules[moduleName] = value.default; return modules; }, {}); const store = new Vuex.Store({ modules, // 挂载modules getters }); export default store;
这种写法同样使用于统一挂载组件,指令,以及过滤器。注意公共组件已经在main.js公共挂载了,使用时无需再次引入,声明
例子
-
在子模块中定义user文件
// 导入获取用户信息接口 import { getUserInfo } from "@/interface/user"; // 定义默认主题 const defaultTheme = "default"; const state = { token: null, // token信息 info: "", // 用户信息 theme: defaultTheme // 默认主题
};
const mutations = {
/**
* 修改token
* @param state state 中存储的数据
* @param token payload 负载(传入信息)
*/
SET_TOKEN: (state, token) => {
state.token = token;
},
/**
* 存储用户信息
* @param state state 中存储的数据
* @param info payload 负载(传入信息)
*/
SET_INFO: (state, info) => {
state.info = info;
},
/**
* 存储用户信息
* @param state state 中存储的数据
* @param theme payload 负载(传入信息)
*/
SET_THEME: (state, theme) => {
state.theme = theme;
}
};
const actions = {
// 获取用户信息
GetUserInfo({commit, state}, loginForm) {
getUserInfo(loginForm).then(res => {
if (res.data === 200) {
// 将用户信息存入store
commit("SET_INFO", { name: res.name, id: res.id });
if (res.theme) {
this.dispatch("ChangeTheme", res.theme);
} else {
this.dispatch("ChangeTheme", defaultTheme);
}
}
});
},
// 登出
Logout({ commit, state }) {
commit("SET_INFO", "");
// 调用登出接口
},
// 改变用户主题
ChangeTheme({ commit, state }, theme) {
commit("SET_THEME", theme);
// 将用户改变的主题数据,存到缓存的userData里 待定(可用插件存储)
const userData = JSON.parse(sessionStorage.getItem("userData"));
sessionStorage.setItem("userData", JSON.stringify({ ...userData, theme: theme }));
// 存到数据库(不方便mock所以省略)
}
};
export default {
namespaced: true,
state,
mutations,
actions
};
```
相关问题汇总
* 计算属性
因为vuex的getter相当于vue的计算属性,我们来过一下计算属性computed
computed相当于多个watch,其中某一个属性变化,就会触发computed,而且computed是会和porp,mixins,以及data同时绑定到this上,也就意味着相当于对computed方法的返回值进行了缓存,需要调用的时候直接使用,不需要重新计算。
对比methods:
computed能实现的,methods肯定能实现
methods每一次调用,都会执行以下代码,如果这段代码的计算量过大没使用computed就是最好的选择
对比watch:
watch可以没有返回值,但是computed必须有返回值
相同:watch,computed都是以vue的依赖追中为基础的,都是根据依赖数据触发函数
不同:watch监听一般是一个数据,从而影响多个数据,computed计算属性是依赖多个值计算一个属性
计算setter
计算属性默认只有 getter ,不过在需要时你也可以提供一个 setter :
computed: { fullName: { // getter get: function () { // fullName 依赖 firstName,lastName 当着两个值发生改变的时候,重新计算fullName return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { //当fullName重新计算的时候会触发 var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } }