目录
一、什么是 Vuex
二、为什么使用 vuex ?(vuex 的使用场景)
三、vuex 的运行机制
四、在 vue 中 使用 vuex——基础版
五、vuex 的核心成员
1、state
(1)、在模块中定义 state 数据
(2)、在组件中获取 state 中的数据的方式
2、getters
(1)、在模块中采用变量的形式定义 getters 的事件
(2)、在组件中使用 getters 的事件
3、mutations
(1)、在模块中采用变量的形式定义 mutations 事件类型
(2)、在组件中提交 Mutation
(3)、使用常量替代 Mutation 事件类型
4、actions
(1)、在模块中采用变量的形式定义 actions 事件类型
(2)、在组件中分发 action
(3)、组合 Action——action 与 Promise 或 async / await
5、module
(1)、模块内部的 mutation 和 getter
(2)、模块内部的 action
(3)、模块的命名空间
6、vuex 的 mapXXX 系列辅助函数
(1)、mapState
(2)、mapGetters
(3)、mapMutations
(4)、mapActions
六、在 vue 中 使用 vuex——进阶版(vuex 的最佳实践)
1、第一步:独立 api、独立 store 后分割模块
2、第二步:将 vuex 注入 vue,并挂载到全局实例上
3、第三步:业务组件与模块一对一开发
(1)、ProductList.vue 业务组件 — products.js 模块
(2)、mutationTypes.js 文件
(3)、cart.js 模块 — ShoppingCart.vue 业务组件
七、vuex 原理解析
1、vuex 是通过什么方式提供响应式数据的?
2、$store 是如何挂载到实例 this 上的?
八、Vue3 的 Vuex——pinia
九、vuex 遇到的问题与解决方案
1、需要使用 state 里某个变量上一次的值
2、不要 commit props 里的初始值,换个思路更合适
Vuex 官网
Vuex 是一种 状态管理模式。
vuex的功能:
安装 Vuex:
npm i -S vuex@next
vue 采用的是单向数据流:
数据驱动视图的更新,用户在视图上操作,触发actions,通过action的方法更改state里的数据。形成一个数据单向流动的闭环。
由于vue是单向数据流,所以,当一个数据被多个组件使用时,而且这个数据还可能被修改,此时这个数据怎么管理呢?
我们想到的是:可以通过共同的父组件作为通信桥梁,实现兄弟组件之间的数据共享,同时,对数据进行集中式管理(如下图)。
通过属性的层层传递的思路是好的,但是实现起来繁琐,而且代码的健壮性不强。于是,只能另辟蹊径:
至此,vuex 的使用场景就出来了:在需要集中式管理数据的大型项目中,用 vuex 来管理业务组件(不是通用组件)。
【拓展】
vuex 采用的也是单向数据流。
由上图可见,vuex 不像 provide/inject 那样写在组件内部,vuex 已经与组件解除强相关了,独立提供响应式数据。
对 vuex 的运行机制的解读:
安装一下 vuex:
npm i vuex -S
然后在main.js中注册 vuex:
import Vuex from 'vuex'
Vue.use(Vuex);
在此,先不将vuex的store单独抽离成一个文件夹,直接在main.js中探索使用vuex。下面,我们通过一个小案例来尝试运用一下vuex:
// main.js
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex'
Vue.config.productionTip = false
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
count: 0,
},
// 同步处理
mutations: {
increment(state){
state.count++;
},
// increment(state, n){
// state.count += n;
// },
},
// 异步处理
actions: {
increment({state}){
setTimeout(()=>{
state.count++;
}, 3000)
},
},
// vuex 中的计算属性,支持缓存
getters: {
doubleCount(state){
return state.count*2;
}
}
})
new Vue({
store,
render: h => h(App),
}).$mount('#app')
// App.vue
{{count}}
{{$store.getters.doubleCount}}
使用方法 | 使用方法的简写 | 描述 | |
---|---|---|---|
state | this.$store.state.XXX 取值 | mapState | 提供一个响应式数据 |
getters | this.$store.getters.XXX 取值 | mapGetters | 借助 Vue 的计算属性 computed 来实现缓存 |
mutations | this.$store.commit("XXX") 赋值 | mapMutations | 更改 state 方法 |
actions | this.$store.dispatch("XXX") 赋值 | mapActions | 触发 mutation 方法 |
module | Vue.set 动态添加 state 到响应式数据中 |
state是存储的单一状态,是存储的基本数据。
state: {
count: 0,
},
{{$store.state.count}}
this.$store.state.count
getters 是 store 的计算属性,是对 state 里的数据加工后生成的新数据。
getters里的方法可以接收 2 个参数,第一个参数是 state,第二个参数是(可选的)其他 getter。
特点与注意:
在组件中,该事件名可以直接作为变量使用)
state: {
count: 0,
},
getters: {
doubleCount(state){
return state.count*2;
}
}
{{$store.getters.doubleCount}}
this.$store.getters.doubleCount
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方。
mutations 里的方法可以接收 2 个参数,第一个参数是 state,第二个参数是额外的参数,即 mutation 的 载荷(payload)。
mutations 提交更改数据,使用store.commit方法更改state存储的状态。
特点与注意:
state.obj = { ...state.obj, newProp: 123 }
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 2)
store.commit({
type: 'increment',
amount: 2
})
使用常量替代 Mutation 事件类型,把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然。
// mutationTypes.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import Vuex from 'vuex'
import { SOME_MUTATION } from './mutationTypes'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我们可以使用 ES6 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
特点与注意:
actions: {
incrementAsync (context) {
setTimeout(() => {
context.commit('increment')
}, 1000)
}
}
上面代码中,actions 里的函数接受一个与 store 实例具有相同方法和属性的 context 对象。因此,你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。
当我们需要多次调用 commit 的时候,建议使用 ES6 的参数解构:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
store.dispatch('incrementAsync', {
amount: 10
})
store.dispatch({
type: 'incrementAsync',
amount: 10
})
store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch 仍旧返回 Promise:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
然后,你可以:
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
于是,如果我们利用 async / await,我们可以如下组合 action:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
注意: 一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
module 是 store 分割的模块,每个模块拥有自己的 state、getters、mutations、actions。
特点与注意:
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象——state。
const moduleA = {
state: () => ({
count: 0
}),
mutations: {
increment (state) {
// 这里的 `state` 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模块内容(module assets)
state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: () => ({ ... }),
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})
进一步学习 module 请戳这里:Module | Vuex
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性。
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?使用对象展开运算符(...):
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
mapGetters 辅助函数用来将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
mapMutations 辅助函数用来将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
})
}
}
mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)。
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')`
})
}
}
了解了 vuex 的核心后,我们尝试着用更加贴合实际开发的法式(包括:独立 api、独立 store 后分割模块、对 mutation 的事件类型使用常量代替变量、给每个模块开启命名空间等等)来使用vuex。
使用 Vuex 的项目结构如下:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action(若开启模块的命名空间,就不必创建此文件了)
├── mutations.js # 根级别的 mutation(若开启模块的命名空间,就不必创建此文件了)
├── mutationTypes.js # 模块中使用常量替代 mutation 事件类型时,需要创建此文件,用来统一管理这些常量
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
从上图可以看出:
【拓展】
本“vuex 的最佳实践”案例是开启模块的命名空间的,所以不用在 store 下创建根级别的 action 和 mutation 文件。
在本“vuex 的最佳实践”案例中,api接口文件夹里有一个shop.js文件,内容如下:
/**
* 模拟客户机-服务器处理
*/
// 模拟服务器返回的数据
const _products = [
{"id": 1, "title": "华为 Mate 20", "price": 3999, "inventory": 2},
{"id": 2, "title": "小米 9", "price": 2999, "inventory": 0},
{"id": 3, "title": "OPPO R17", "price": 2999, "inventory": 5}
]
export default {
// 获取所有商品
getProducts (cb) {
// 模拟异步请求
setTimeout(() => cb(_products), 100)
},
// 购买商品
buyProducts ( products, cb, errorCb) {
// 模拟异步请求
setTimeout(() => {
// 模拟随机检查,成功执行成功的回调,失败执行失败的回调。
Math.random() > 0.5
? cb()
: errorCb()
}, 100)
}
}
在 store 的 index.js 文件中引入 vuex,并注入 vue,然后在 main.js 中注册 store。
// index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
// 如果有管理用户信息这个需求的话,最好将其放在总的state里进行管理
userInfo: {
email: "[email protected]",
},
},
});
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'// 引入store,默认会指向 index
Vue.config.productionTip = false
new Vue({
store,// 将 store 绑定到 vue 实例上,使其全局可用
render: h => h(App),
}).$mount('#app')
在本 “vuex 的最佳实践”案例中,采用了 mapXXX 系列辅助函数。
业务组件 ProductList.vue 中:
// 产品列表
-
{{ product.title }} - {{ product.price }}
products.js 模块:
import shop from "../../api/shop";
// import { PRODUCTS } from "../mutations";
const state = {
all: [],
};
const getters = {};
/** @type {*} */
const actions = {
/**
* 处理异步的地方
*/
// 获取所有的产品
getAllProducts({ commit }) {
// 调用api里的shop里的getProducts方法
shop.getProducts((products) => {
commit(PRODUCTS.SET_PRODUCTS, products);
// commit("setProducts", products);
});
},
};
const mutations = {
/**
* mutations事件类型采用“常量代替变量”,注释掉的部分是变量,
* 并将常量统一汇总到store/mutations里,便于管理。
*/
[PRODUCTS.SET_PRODUCTS](state, products) {
state.all = products;
},
// setProducts(state, products){
// state.all = products;
// },
[PRODUCTS.DECREMENT_PRODUCT_INVENTORY](state, { id }) {
const product = state.all.find((product) => product.id === id);
product.inventory--;
},
// decrementProductInventory(state, { id }){
// const product = state.all.find((product) => product.id === id);
// product.inventory--;
// }
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
这里只是展示一下,方便上下文阅读代码:
export const CART = {
PUSH_PRODUCT_TO_CART: "pushProductToCart",
INCREMENT_ITEM_QUANTITY: "incrementItemQuantity",
SET_CART_ITEMS: "setCartItems",
SET_CHECKOUT_STATUS: "setCheckoutStatus",
};
export const PRODUCTS = {
SET_PRODUCTS: "setProducts",
DECREMENT_PRODUCT_INVENTORY: "decrementProductInventory",
};
cart.js 模块:
import shop from "../../api/shop";
import { CART, PRODUCTS } from "../mutationTypes";
const state = {
items: [],
checkoutStatus: null,
};
const getters = {
cartProducts: (state, getters, rootState) => {
return state.items.map(({ id, quantity }) => {
const product = rootState.products.all.find(
(product) => product.id === id
);
return {
title: product.title,
price: product.price,
quantity,
};
});
},
cartTotalPrice: (state, getters) => {
return getters.cartProducts.reduce((total, product) => {
return total + product.price * product.quantity;
}, 0);
},
};
const actions = {
checkout({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.items];
// 发出结账请求,然后清空购物车
commit(CART.SET_CHECKOUT_STATUS, null);
// empty cart
commit(CART.SET_CART_ITEMS, { items: [] });
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(CART.SET_CHECKOUT_STATUS, "successful"),
// 失败操作
() => {
commit(CART.SET_CHECKOUT_STATUS, "failed");
// rollback to the cart saved before sending the request
commit(CART.SET_CART_ITEMS, { items: savedCartItems });
}
);
},
addProductToCart({ state, commit }, product) {
commit(CART.SET_CHECKOUT_STATUS, null);
if (product.inventory > 0) {
const cartItem = state.items.find((item) => item.id === product.id);
if (!cartItem) {
commit(CART.PUSH_PRODUCT_TO_CART, { id: product.id });
} else {
commit(CART.INCREMENT_ITEM_QUANTITY, cartItem);
}
// remove 1 item from stock
commit(
`products/${PRODUCTS.DECREMENT_PRODUCT_INVENTORY}`,
{ id: product.id },
{ root: true }
);
}
},
};
const mutations = {
[CART.PUSH_PRODUCT_TO_CART](state, { id }) {
state.items.push({
id,
quantity: 1,
});
},
// pushProductToCart(state, { id }){
// state.items.push({
// id,
// quantity: 1,
// });
// },
[CART.INCREMENT_ITEM_QUANTITY](state, { id }) {
const cartItem = state.items.find((item) => item.id === id);
cartItem.quantity++;
},
// incrementItemQuantity(state, { id }){
// const cartItem = state.items.find((item) => item.id === id);
// cartItem.quantity++;
// },
[CART.SET_CART_ITEMS](state, { items }) {
state.items = items;
},
// setCartItems(state, { items }){
// state.items = items;
// },
[CART.SET_CHECKOUT_STATUS](state, status) {
state.checkoutStatus = status;
},
// setCheckoutStatus(state, status){
// state.checkoutStatus = status;
// }
};
export default {
namespaced: true,
state,
getters,
actions,
mutations,
};
业务组件 ShoppingCart.vue 中:
// 购买清单
清单
请添加产品到购物车
-
{{ product.title }} - {{ product.price }} x {{ product.quantity }}
合计: {{ total }}
提交 {{ checkoutStatus }}.
vue 的双向绑定原理
Vuex 的双向绑定通过调用 new Vue实现,然后将 this.$store 对象通过 Vue.mixin 注入到 Vue 所有组件的 beforeCreate 生命周期中,最后通过 Object.defineProperty() 方法对数据进行劫持——通过在 state.get() 或 state.set() 方法来获取或设置组件中的数据。
import Vue from 'vue'
const Store = function Store (options = {}) {
const {state = {}, mutations = {}} = options;
// 借用 vue 实现数据的响应式绑定
this._vm = new Vue({
data: {
$$state: state
},
})
this._mutations = mutations
}
// 在Store的原型上创建commit方法,调用_mutations里的方法
Store.prototype.commit = function (type, payload) {
if(this._mutations[type]){
this._mutations[type](this.state, payload)// 传递state和一个额外的参数
}
}
/**
* Object.defineProperties() 方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。
* 重写Store原型上的state的get方法,令其指向this._vm._data.$$state。
*/
Object.defineProperties(Store.prototype, {
state: {
get: function () {
return this._vm._data.$$state
}
}
})
export default {Store}
在vue项目中先安装vuex,核心代码如下:
import Vuex from 'vuex';
Vue.use(vuex);// vue的插件机制
利用vue的插件机制,使用Vue.use(vuex)时,会调用vuex的install方法,装载vuex,install方法的代码如下:
export function install (_Vue) {
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue
applyMixin(Vue)
}
applyMixin方法使用vue混入机制,vue的生命周期beforeCreate钩子函数前混入vuexInit方法,核心代码如下:
Vue.mixin({ beforeCreate: vuexInit });
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
分析源码,我们知道了vuex是利用vue的mixin混入机制,在beforeCreate钩子前混入vuexInit方法,vuexInit方法实现了store注入vue组件实例,并注册了vuex store的引用属性$store。store注入过程如下图所示:
pinia 被誉为 vuex5。
请参阅这篇文章 Vuex5——pinia
用 getter 来解决这个问题。getter 是 vuex 的“计算属性”。
例如:
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
],
currentDo: '',
},
getter: {
doneTodos (state) {
return state.todos.filter(todo => todo.done);
}
},
mutations: {
changeCurrentDo(state, payload) {
state.currentDo = payload.currentItem || doneTodos;
}
}
})
假设,需要在 vue 的 mounted 生命周期中执行一次 commit 来更新 state 里的变量, 但该值并没有更新,首次拿到的还是初始值,这就造成 vuex 的赋值时机延迟了。
根本原因是:子组件渲染的时候父组件还未取到值,导致子组件取不到数据——子组件 created 和 mounted 只执行一次 ,在父组件初始化时,已经给子组件传了一个空值,导致异步请求数据发生变化时,子组件的值不再变化。
问题再现:
store 中:
const store = createStore({
state: {
list: []
},
mutations: {
changeList(state, payload) {
const { baseValList } = payload;
Vue.set(state, 'list', baseVal);
}
}
})
父组件中:
子组件中:
此时,发现:“在 mounted 里调用的 commit 总是会先将 baseValList 的初始值(即:[])在 changeList 里赋值给 list,之后才会将 [{id:1}] 赋值给 list。”
如果你的需求是:当 baseValList 不为空时才显示,那么你可以在父组件上增加一个v-if 来解决上述问题。否则下面这种解决方案会导致 baseValList 为空数组时该子组件消失了。
从更高的维度上来看:既然用了 vuex 那为什么还传 props 呢?此时完全可以不必再传 props。直接在父组件中 commit 这个 baseValList 到 vuex 中即可。
【推荐文章】
vuex 的工作原理
用一句话说明 Vuex工作原理
vuex官网
父组件从vuex获取数据给子组件传值延迟问题