状态管理工具
状态管理工具出现的背景
- 组件间数据共享的问题
- 单向数据流:
- 多个视图依赖于同一个状态。传参方法:
props,emit
。对于多层嵌套的组件会非常繁琐,同时无法解决兄弟组件间的状态共享问题 - 来自不同视图的行为需要变更同一个状态。事件方法:
emit, on
。可以采用父子组件直接引用或者通过事件来变更和同步状态的拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
- 多个视图依赖于同一个状态。传参方法:
- 全局对象
- 比如 window,也可以全局获取和修改,但是全局对象会污染全局环境,且多个组件可能都需要修改它,这样不利于维护。而且没办法让组件知道数据的变化,导致需要手动刷新组件。
// 在所有组件外层套一个父组件,所有子组件都可以共享子组件内的状态
const Count = {
// 状态
data() {
return {
count: 0,
};
},
// 视图
template: `
<div>
<button @click="count++">+1</button>
{{count}}
</div>
`,
methods: {
increment() {},
},
};
createApp(Count).mount("#app");
- 状态管理工具(vuex, redux, mobx)
Vuex
什么是状态管理工具
- 状态管理工具是一种集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
什么是 Vuex
一个专为Vue.js
应用程序开发的状态管理模式 + 库。采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
- Vuex 是集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- Vuex 是响应式的,当数据变化时,视图会重新渲染。
核心思想
-
集中式状态管理 Vuex 将所有组件的共享状态抽取出来,集中存储在一个 store 中,使状态的变更变得可追踪,可预测,并可统一管理
-
状态响应式的实现原理 Vuex 内部使用 Vue 的响应式系统(Vue2 使用
Object.defineProperty
,Vue3 使用Proxy
)来让state
中的对象变为响应式的。组件从store
中获取状态后,一旦state
中的数据发生变化,那么组件也会自动更新。
Vuex 的基本使用
状态自管理应用包含以下几个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
在 vuex 中,数据的流动是单向的,遵循State -> View → Action/Mutations → State
的循环
组件触发事件 -→ Action(处理异步逻辑) -→ Mutation(修改状态) --> State(存储数据) --> View(渲染 UI)
View(组件) │ dispatch(异步)/commit(同步) ↓ actions → mutations → state(被更新) ↑ getters(派生状态) │ View(再次获取新状态)
组件 dispatch → actions → commit → mutations → 修改 state(响应式)→ 组件自动更新
- State: 存放共享数据的地方
- Getters: 类似于计算属性,用于获取 state 中的数据
- Mutations: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。每个 mutation 都有一个字符串的事件类型 (type) 和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
- Actions: 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。
VueX 的核心概念
- Vuex 的核心概念包括:state、getters、mutations、actions、modules。
- Vuex 的状态存储是响应式的。当 vue 组件从 store 中读取状态的时候,如果 store 中的状态发生变化,那么响应的组件也会触发更更新。
- Vuex 的状态存储是响应式的。当 vue 组件从 store 中读取状态的时候,如果 store 中的状态发生变化,那么响应的组件也会触发更更新。
- 不能直接修改 store 中的状态,而是需要通过 mutation 来进行状态的修改,这是改变 store 状态的唯一方式(
commit
)。方便跟踪每一个状态的变化,从而让我们能够更好地理解我们的应用发生了什么变化,并且更好地进行调试。
import { createApp } from "vue";
import { createStore } from "vuex";
// 创建一个 store 实例
const store = createStore({
state() {
return {
count: 0,
};
},
mutations: {
increment(state) {
state.count++;
},
},
});
const app = createApp("#app");
// 将 store 实例作为插件注入到根组件
app.use(store);
- 接下来,可以在任何组建中通过
store.xxx
获取状态对象,并通过commit
触发变更
store.commit("increment");
console.log(store.state.count); // -> 1
// 访问 store 实例
methods:{
increment(){
this.$store.commit("increment")
console.info(this.$store.state.count) // -> 1
}
}
- 我们可以直接从
store
中获取状态,但是,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,以一个全局 store 对象管理应用的所有状态,而不管状态是定义在哪个模块,这会导致代码变得难以维护。
// 一个Vue组件
const Counter = {
template: `<span>Count is: {{ count }}</span>`,
computed: {
count() {
return store.state.count;
},
},
};
- vuex 通过 Vue 的插件系统将
store
实例从根组件注入到每一个组件,实现组件与store
的通信,从而实现组件与组件之间的状态共享。
const Counter = {
template: `<span>Count is: {{ count }}</span>`,
computed: {
count() {
// 子组件中直接访问 store 实例
return this.$store.state.count;
},
},
};
- 当一个组件需要获取多个状态的时候,我们可以使用
mapState
辅助函数帮助我们生成计算属性。
import { mapState } from "vuex";
export default {
computed: {
...mapState({
count: (state) => state.count,
// 可以直接传递一个字符串参数
countAlias: "count", // 等价于 count: state => state.count
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState(state) {
return state.count + this.localCount; // this.localCount 是组件的局部状态
},
}),
},
};
// 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
computed: {
...mapState(["count"]),
},
// mapState 函数返回一个对象,因此可以与局部计算属性混合使用
computed: {
localComputed() {},
...mapState({
count: (state) => state.count,
}),
},
State
: 定义了应用状态的数据结构,可以在这里设置默认的初始状态。
Getters
: store 的计算属性
- 允许组件从 Store 中获取状态,mapGetters 辅助函数可以将 store 中的 getter 映射到局部计算属性。
// getter 可以认为是 store 的计算属性,接受store为第一个参数
const store = createStore({
state: {
todos: [
{ id: 1, text: "...", done: true },
{ id: 2, text: "...", done: false },
],
},
getters: {
filterList(state) {
return state.todos.filer((todo) => todo.done);
},
},
});
// 调用 getters
store.getters.filterList; // -> [{ id: 1, text: "...", done: true }]
/* ********** */
// getters 可以接受其他 getter 作为第二个参数
getters: {
// ...
todosCount(state, getters){
return getters.filterList.length
}
}
// 代码中访问
store.getters.todosCount; // -> 1
// 组件中访问
computed: {
// ...
todosCount() {
return this.$store.getters.todosCount; // -> 1
}
}
- 通过方法访问
// 定义 getters
getters:{
// ...
getListById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
// 使用 getters
store.getters.getListById(1); // -> { id: 1, text: "...", done: true }
// 组件中访问
computed: {
// ...
getGetterListById() {
return this.$store.getters.getListById(1);
}
}
- mapGetters 辅助函数,仅仅是将 store 中的 getter 映射到局部计算属性
import { mapGetters } from "vuex";
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
// 调用前面写过的 getter 方法
...mapGetters([
"todosCount",
"filterList",
"getListById"
// ...
]),
},
};
// 给 getter属性另取名字
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: "doneTodosCount",
})
Mutations
: 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation 是唯一允许更新应用状态的地方,它接受 state 作为第一个参数,接受额外参数作为第二个参数。
const store = createStore({
state: {
count: 0,
},
mutations: {
increment(state) {
// 变更状态
state.count++;
},
},
});
// 调用 mutation
store.commit("increment"); // -> state.count + 1
- 我们不能直接调用一个
mutation
处理函数,需要通过commit
来触发一个mutation
处理函数,mutation
处理函数必须是同步函数。(这个选项更像事件注册) - 提交参数(payload):
mutations: {
increment(state, n) {
state.count += n;
},
}
store.commit("increment", 233)
/* ******* */
// payload 应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
mutations: {
increment(state, payload) {
state.count += payload.amount;
},
}
store.commit("increment", {
amount: 10,
})
// 对象风格的提交方式
store.commit({
type: "increment",
amount: 10,
})
- 作为代码规范,我们需要使用常量替代
mutation
事件类型
// mutation-type.js
export const SOME_MUTATION = "SOME_MUTATION";
// store.js
import Vuex from "vuex";
import { SOME_MUTATION } from "./mutation-type";
const store = new Vuex.Store({
state: {
//...
},
mutations: {
// 使用 es2015 风格的计算属性命名功能来使用一个常量作为函数名
[SOME_MUTATION](state, payload) {
// mutate state
state.count += payload.amount;
},
},
});
Mutation
必须是同步函数,这是为了调试和跟踪状态变化。
mutations: {
increment(state) {
setTimeout(() => {
state.count++;
}, 1000); // 异步操作不行
}
someMutation(state) {
api.callAsyncData(()=>{
state.count++; // 异步操作不行
})
}
}
mapMutations
辅助函数,在组件中提交mutation
:
import { mapMutations } from "vuex";
export default {
// ...
methods: {
// 将 `this.increment()` 映射为 `this.$store.commit('increment')`
...mapMutations(["increment"]),
// 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
...mapMutations({
incrementBy: "increment",
}),
},
};
Actions
:用于提交 mutation,而不是直接变更状态,可以包含任意异步操作。
- Actions 提交的是 mutation,而不是直接变更状态。
- Actions 可以包含任意异步操作。
const store = createStore({
state: {
count: 0,
},
mutations: {
increment(state) {
state.count++;
},
},
actions: {
increment(context) {
// context 对象具有和 store 实例相同的方法属性,但并不是 store 实例本身
context.commit("increment");
},
},
});
// 实际写法
actions: {
increment({ commit }) {
commit("increment");
},
},
- 使用 Action:
store.dispatch("increment");
- 可以在 Action 内部执行异步操作
// 使用 Action
store.dispatch("increment");
// Action 内部执行异步操作
actions: {
incrementAsync({commit}){
setTimeout(() => {
commit("increment");
}, 100);
}
}
// 以 payload 的形式提交
store.dispatch("incrementAsync", {
amount: 10,
})
// 以对象形式提交
store.dispatch({
type: "incrementAsync",
amount: 10,
})
/* ********* */
// e.g.:
actions:{
checkout({ commit, state }, products) {
// 将当前物品备份
const savedCartItems = [...state.cart.added];
// 发起 checkout 请求,然后使用备份的物品,清空购物车
commit(types.CHECKOUT_REQUEST);
shop.buyProducts(
products,
() => {
// 请求成功
commit(types.CHECKOUT_SUCCESS);
},
() => {
// 请求失败,恢复 cart 状态
commit(types.CHECKOUT_FAILURE, savedCartItems);
}
);
},
};
- 在组件中使用 actions
import { mapActions } from "vuex";
export default {
methods: {
...mapActions(["increment"]),
// 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
...mapActions({
incrementBy: "increment",
}),
},
};
- 组合多个 actions
// 如何组合多个 actions
actions:{
actionA({ commit }) {
return new Promise((res, rej) => {
setTimeout(() => {
commit("someMutation");
res();
}, 100);
});
},
};
store.dispatch("actionA").then(() => {
// 做一些actionA执行完之后的事情...
});
// 在 actions 中分发其他 actions
actions: {
// ...
actionB({ dispatch, commit }) {
return dispatch("actionA").then(() => {
commit("someOtherMutation");
});
},
};
/* ****** */
// 更推荐结合 async/await
actions: {
async actionA({commit}){
commit('gotData',await getData())
}
async actionB({dispatch,commit}){
await dispatch('actionA')
// 等待 actionA 完成之后再执行其他操作
commit('gotOtherData',await getOtherData())
}
}
Modules
: 使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。 Vuex 允许将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块。
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
// 挂载
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
- 模块的局部状态
const moduleA = {
state:()=>{
return{
count:0,
id:2333
}
}
mutations:{
// 这里 state 是模块的局部状态
increment(state){
state.count++
}
}
actions:{
// 对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
async increment({ state, commit, rootState }){
await { data } = api.fetchData({id:state.id})
// ...
if(state.count < 10){
commit('doubleCount')
}else{
rootState.count++
}
}
}
// 模块内部的 getter,根节点状态会作为第三个参数暴露出来:
getters:{
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
doubleCount(state){
return state.count * 2
}
}
}
namespace
命名空间: 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的 —— 这样使得多个模块能够对同一 mutation 或 action 作出响应。 可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
// 命名空间
const store = createStore({
modules: {
account: {
namespaced: true, // 开启命名空间
state: () => {},
getters: {
isAdmin() {
// 调用方法: getters['account/isAdmin']
// getters['account/isAdmin']
},
},
actions: {
logIn() {
// 调用方法: dispatch('account/logIn')
// dispatch('account/logIn')
},
},
mutations: {
logOut() {
// 调用方法: commit('account/logOut')
// commit('account/logOut')
},
},
},
},
});
// 嵌套模块的命名空间
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']
}
}
}
}
}
})
结构
--- main.js
--- api
--- userData.js
--- HomeData.js
--- App.vue
--- User.vue
--- store
--- index.js // 组装模板并导出 store 的地方
Vuex 对比组件通信
特性 | Vuex | 父子通信 / Event Bus / Props 等 |
---|---|---|
适合场景 | 跨组件复杂状态共享 | 简单数据传递或局部状态管理 |
状态追踪 | 清晰,支持时间旅行调试 | 难以跟踪 |
数据流向 | 单向 | 可变(可能出现回调地狱) |