在 Vue 中,数据驱动和组件化是最重要的,每个组件都有自己的 data、template 和 methods。data 是数据,我们也叫做状态,通过组件的方法改变状态来更新视图。在单个组件中修改状态更新视图是很方便的。
然而,当应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
-
多个视图依赖于同一状态;
-
来自不同视图的行为需要变更同一状态;
对于第一个问题,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于第二个问题,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。这些模式非常脆弱,通常会导致难以维护的代码。
因此,为什么不把组件的共享状态抽取出来,以全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的视图,任何组件都能获取状态或者触发行为。
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,代码将会变得更结构化且易维护,这就是 Vuex 背后的基本思想。

状态管理应用包含以下几个部分:
-
状态:驱动应用的数据源;
-
视图:以声明方式将状态映射到视图;
-
操作:响应在视图上的用户输入导致的状态变化;

1. Vuex介绍及深入
1.1. 安装vuex
通过 yarn create vite 创建 Vue 工程,安装 Vuex:
yarn add vuex
每一个 Vuex 应用的核心就是仓库 store,store基本上是一个容器,包含了应用中大部分的状态state。
Vuex 和全局对象有以下两点不同:
-
Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态时,若 store 中的状态发生变化,相应的组件也会高效更新;
-
你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径是显式地提交 mutation,这样可以方便地跟踪每一个状态的变化;
1.2. 最简单的store
javascript
// store.js
import { createStore } from 'vuex';
const defaultState = {
count: 0,
};
export default createStore({
state() {
return defaultState;
},
mutations: {
increment(state) {
state.count++;
},
},
actions: {
increment(context) {
context.commit('increment');
},
},
});
// app.js
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';
createApp(App).use(store).mount('#app');
1.3. 单一状态树
Vuex 使用单一状态树,一个对象包含全部的应用层级状态。这样每个应用仅包含一个 store 实例,可以直接定位任一特定状态片段,调试时也能轻易获取当前应用状态的快照。
1.4. 在 Vue 组件中获得 Vuex 状态
最简单的方法是在计算属性中返回某个状态:
javascript
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return this.$store.state.count;
}
}
}
1.5. mapState 辅助函数
一个组件需要获取多个状态时,可以使用 mapState 辅助函数生成计算属性:
javascript
import { mapState } from 'vuex';
export default {
computed: mapState({
count: state => state.count,
countAlias: 'count',
countPlusLocalState(state) {
return state.count + this.localCount;
}
})
}
1.6. 组件仍然保有局部状态
使用 Vuex 并不意味着需要将所有状态放入 Vuex,如果状态严格属于单个组件,最好作为组件的局部状态,根据应用开发需要进行权衡。
2. Vuex 核心概念
2.1. Getter
从 store 中派生状态:
javascript
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos(state) {
return state.todos.filter(todo => todo.done);
}
}
});
2.2. Mutation
更改 Vuex 的 store 中的状态的唯-方法是提交 mutation:
javascript
const store = createStore({
state: {
count: 1
},
mutations: {
increment(state) {
state.count++;
}
}
});
store.commit('increment');
2.3. Actions
Actions存在的意义是假设你在修改state的时候有异步操作,vuex不希望你将异步操作放在Mutations中,所以就给你设置了一个区域,让你执行异步操作,这就是Actions。
javascript
const store = createStore({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
}
})
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。当我们在之后介绍到 Modules时,你就知道 context 对象为什么不是 store 实例本身了。
实践中,我们会经常用到 ES2015 的参数解构来简化代码,特别是我们需要调用 commit 很多次的时候。
javascript
actions: {
increment({ commit }) {
commit('increment')
}
}
Action 通过 store.dispatch 方法触发:
javascript
store.dispatch( increment')
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束,我们可以在 action 内部执行异步操作:
javascript
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Actions 支持同样的载荷方式和对象方式进行分发:
javascript
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在组件中使用 this.$store.dispatch('xxx)分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用,前提是需要先在根节点注入 store:
javascript
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy')`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}
}
2.4. Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
javascript
// 从上至下进行同样方式的分割
const moduleA = {
state: () => ({ count: 0 }),
mutations: { increment(state) { state.count++; } },
getters: { doubleCount(state) { return state.count * 2; } }
};
const moduleB = {
state: () => ({ count: 0 }),
mutations: { increment(state) { state.count++; } },
actions: { incrementIfOddOnRootSum({ state, commit, rootState }) { if ((state.count + rootState.count) % 2 == 1) { commit('increment'); } } }
};
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
});
store.state.a;
store.state.b;
2.5. 命名空间
默认情况下,模块内部的 action 和 mutation 仍然是注册在全局命名空间的,这样使得多个模块能够对同一个 action 或 mutation 作出响应。Getter 同样也默认注册在全局命名空间,但是目前这并非出于功能上的目的,仅仅是维持现状来避免非兼容性变更。必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespace: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
javascript
const moduleA = {
namespace: true,
state: () => ({ count: 0 }),
mutations: { increment(state) { state.count++; } },
getters: { doubleCount(state) { return state.count * 2; } }
};
const store = createStore({
modules: {
a: moduleA
}
});