Vuex
# Vuex
# 1. introduction
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- 什么是“状态管理模式”?
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
- Vuex 核心思想
- Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)
- Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
- 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
# 2. Vuex 初始化
安装
当我们在代码中通过 import Vuex from 'vuex'
的时候,实际上引用的是一个对象,它的定义在 src/index.js 中:
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
2
3
4
5
6
7
8
9
10
和 Vue-Router 一样,Vuex 也同样存在一个静态的 install 方法,它的定义在 src/store.js 中:
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)
}
2
3
4
5
6
7
8
9
10
11
12
install 的逻辑很简单,把传入的 _Vue 赋值给 Vue 并执行了 applyMixin(Vue)
方法,执行 Vue.mixin({ beforeCreate: vuexInit })。
它其实给 Vue 全局混入了一个 beforeCreate 钩子函数,它的实现非常简单,就是把 options.store 保存在所有组件的 this.$store 中,这个 options.store 就是我们在实例化 Store 对象的实例。
Store 实例化
- 用法
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== "production",
modules: {
moduleA
},
state: initPageState(),
mutations: {},
actions: {}
});
export default store;
2
3
4
5
6
7
8
9
10
11
Store 对象的构造函数也是一个 class,接收一个对象参数,它包含 actions、getters、state、mutations、modules 等 Vuex 的核心概念
初始化模块
Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块——从上至下进行同样方式的分割
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... },
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
从数据结构上来看,模块的设计就是一个树型结构,store 本身可以理解为一个 root module,它下面的 modules 就是子模块,Vuex 需要完成这颗树的构建。
构建过程的入口
this._modules = new ModuleCollection(options)
- 调用 register 方法,通过
const newModule = new Module(rawModule, runtime)
创建了一个Module
的实例,Module
是用来描述单个模块的类。 register
首先根据路径获取到父模块,然后再调用父模块的addChild
方法建立父子关系。register
的最后一步,就是遍历当前模块定义中的所有modules
,根据key
作为path
,递归调用register
方法,这样就建立父子关系。
安装模块
对模块中的 state、getters、mutations、actions 做初始化工作 它的入口代码是:
const state = this._modules.root.state;
installModule(this, state, [], this._modules.root);
2
- 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
- 如果我们希望模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
- 构造了一个本地上下文环境:
const local = module.context = makeLocalContext(store, namespace, path);
- registerMutation
- registerAction
- registerGetter
总结: 所以 installModule
实际上就是完成了模块下的 state、getters、actions、mutations
的初始化工作,并且通过递归遍历的方式,就完成了所有子模块的安装工作。
初始化
store._vm
Store 实例化的最后一步,就是执行初始化 store._vm 的逻辑,它的入口代码是:
resetStoreVM(this, state);
resetStoreVM 的作用实际上是想建立 getters 和 state 的联系,因为从设计上 getters 的获取就依赖了 state ,并且希望它的依赖能被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。因此这里利用了 Vue 中用 computed 计算属性来实现。
strict mode
当严格模式下,store._vm 会添加一个 wathcer 来观测 this._data.$state 的变化,也就是当 store.state 被修改的时候, store._committing 必须为 true,否则在开发阶段会报警告。
if (store.strict) {
enableStrictMode(store)
}
function enableStrictMode (store) {
store._vm.$watch(function () { return this._data.$$state }, () => {
if (process.env.NODE_ENV !== 'production') {
assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
2
3
4
5
6
7
8
9
10
11
总结
我们要把 store 想象成一个数据仓库,为了更方便的管理仓库,我们把一个大的 store 拆成一些 modules,整个 modules 是一个树型结构。每个 module 又分别定义了 state,getters,mutations、actions,我们也通过递归遍历模块的方式都完成了它们的初始化。
为了 module 具有更高的封装度和复用性,还定义了 namespace 的概念。最后我们还定义了一个内部的 Vue 实例,用来建立 state 到 getters 的联系,并且可以在严格模式下监测 state 的变化是不是来自外部,确保改变 state 的唯一途径就是显式地提交 mutation。
# 3. API
数据获取
- Vuex 最终存储的数据是在 state 上的,我们之前分析过在 store.state 存储的是 root state,那么对于模块上的 state,假设我们有 2 个嵌套的 modules,它们的 key 分别为 a 和 b,我们可以通过 store.state.a.b.xxx 的方式去获取。
- 在递归执行 installModule 的过程中,就完成了整个 state 的建设,这样我们就可以通过 module 名的 path 去访问到一个深层 module 的 state。
数据存储
- Vuex 对数据存储的存储本质上就是对 state 做修改,并且只允许我们通过提交 mutaion 的形式去修改 state。
- mutation 必须是同步函数
- action
- action 类似于 mutation,不同在于 action 提交的是 mutation,而不是直接操作 state,并且它可以包含任意异步操作。
- action 比我们自己写一个函数执行异步操作然后提交 muataion 的好处是在于它可以在参数中获取到当前模块的一些方法和状态,Vuex 帮我们做好了这些。
语法糖
- mapState
mapState 支持传入 namespace, 因此我们可以这么写:
computed: {
mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
2
3
4
5
6
在 mapState
的实现中,如果有 namespace
,则尝试去通过 getModuleByNamespace(this.$store, 'mapState', namespace)
对应的 module
,然后把 state
和 getters
修改为 module
对应的 state
和 getters
主要原因是在 Vuex
初始化执行 installModule
的过程中,初始化了这个映射表:
function installModule (store, rootState, path, module, hot) {
// ...
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// ...
}
2
3
4
5
6
7
8
9
10
11
- mapGetters
mapGetters 的用法:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
和 mapState 类似,mapGetters 是将 store 中的 getter 映射到局部计算属性
mapGetters 也同样支持 namespace,如果不写 namespace ,访问一个子 module 的属性需要写很长的 key,一旦我们使用了 namespace,就可以方便我们的书写,每个 mappedGetter 的实现实际上就是取 this.$store.getters[val]
。
- mapMutations
我们可以在组件中使用 this.$store.commit('xxx')
提交 mutation
,或者使用 mapMutations
辅助函数将组件中的 methods
映射为 store.commit 的调用。mapMutations
支持传入一个数组或者一个对象,目标都是组件中对应的 methods
映射为 store.commit
的调用。
mapMutations 的用法:
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')`
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mappedMutation 同样支持了 namespace,并且支持了传入额外的参数 args,作为提交 mutation 的 payload,最终就是执行了 store.commit 方法,并且这个 commit 会根据传入的 namespace 映射到对应 module 的 commit 上。
- mapActions
在组件中使用 this.$store.dispatch('xxx')
提交 action
,或者使用 mapActions
辅助函数将组件中的 methods
映射为 store.dispatch
的调用。
mapActions
在用法上和 mapMutations
几乎一样,实现也很类似,和 mapMutations 的实现几乎一样,不同的是把 commit
方法换成了 dispatch。
动态更新模块
- 模块动态注册 registerModule
在有一些场景下,我们需要动态去注入一些新的模块,Vuex 提供了模块动态注册功能,在 store 上提供了一个 registerModule 的 API。
registerModule 支持传入一个 path 模块路径 和 rawModule 模块定义,首先执行 register 方法扩展我们的模块树,接着执行 installModule 去安装模块,最后执行 resetStoreVM 重新实例化 store._vm,并销毁旧的 store._vm。
- 动态卸载模块 unregisterModule
相对的,有动态注册模块的需求就有动态卸载模块的需求,Vuex 提供了模块动态卸载功能,在 store 上提供了一个 unregisterModule 的 API。
- unregisterModule 支持传入一个 path 模块路径,首先执行 unregister 方法去修剪我们的模块树。 注意,这里只会移除我们运行时动态创建的模块。
- 接着会删除 state 在该路径下的引用,最后执行 resetStore 方法。
- 该方法就是把 store 下的对应存储的
_actions、_mutations、_wrappedGetters 和 _modulesNamespaceMap
都清空,然后重新执行installModule
安装所有模块以及resetStoreVM
重置store._vm
。
# 4. 插件
Vuex 除了提供的存取能力,还提供了一种插件能力,让我们可以监控 store 的变化过程来做一些事情。
Vuex 的 store 接受 plugins 选项,我们在实例化 Store 的时候可以传入插件,它是一个数组,然后在执行 Store 构造函数的时候,会执行这些插件:
const {
plugins = [],
strict = false
} = options
// apply plugins
plugins.forEach(plugin => plugin(this));
2
3
4
5
6
Logger 插件
Logger 函数,它相当于订阅了 mutation 的提交,它的 prevState 表示之前的 state,nextState 表示提交 mutation 后的 state,这两个 state 都需要执行 deepCopy 方法拷贝一份对象的副本,这样对他们的修改就不会影响原始 store.state。
接下来就构造一些格式化的消息,打印出一些时间消息 message, 之前的状态 prevState,对应的 mutation 操作 formattedMutation 以及下一个状态 nextState。
最后更新 prevState = nextState,为下一次提交 mutation 输出日志做准备。
总结
Vuex 从设计上支持了插件,让我们很好地从外部追踪 store 内部的变化,Logger 插件在我们的开发阶段也提供了很好地指引作用。