Vuex4 核心概念
State
单一状态树
Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。
这也意味着,每个应用将仅仅包含一个 store 实例。
存储在 Vuex 中的数据和 Vue 实例中的 data
遵循相同的规则,例如状态对象必须是纯粹 (plain) 的。
在 Vue 组件中获得 Vuex 状态
Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态
1 | // 创建一个 Counter 组件 |
每当 store.state.count
变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
然而,这种模式导致组件依赖全局状态单例。
在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。
Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里。且子组件能通过 this.$store
访问到。
1 | const Counter = { |
mapState
辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性
1 | // 在单独构建的版本中辅助函数为 Vuex.mapState |
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组。
1 | computed: mapState([ |
对象展开运算符
1 | computed: { |
组件仍然保有局部状态
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。
如果有些状态严格属于单个组件,最好还是作为组件的局部状态。
你应该根据你的应用开发需要进行权衡和确定。
Getter
从 store 中的 state 中派生出一些状态
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。
Getter 接受 state 作为其第一个参数:
1 | const store = createStore({ |
通过属性访问
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值
1 | store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }] |
Getter 也可以接受其他 getter 作为第二个参数
1 | getters: { |
1 | store.getters.doneTodosCount // -> 1 |
可以很容易地在任何组件中使用它
1 | computed: { |
getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的
通过方法访问
可以通过让 getter 返回一个函数,来实现给 getter 传参。在对 store 里的数组进行查询时非常有用。
1 | getters: { |
1 | store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false } |
mapGetters
辅助函数
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性
1 | import { mapGetters } from 'vuex' |
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。
Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的**事件类型 (type)和一个回调函数 (handler)**。
这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数。
1 | const store = createStore({ |
不能直接调用一个 mutation 处理函数。
要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法
1 | store.commit('increment') |
提交载荷(Payload)
你可以向 store.commit
传入额外的参数,即 mutation 的载荷(payload)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
1 | mutations: { |
1 | store.commit('increment', { |
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type
属性的对象
1 | store.commit({ |
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变
1 | mutations: { |
使用常量替代 Mutation 事件类型
这样做可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然
1 | // mutation-types.js |
1 | // store.js |
Mutation 必须是同步函数
任何在回调函数中进行的状态的改变都是不可追踪的
在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
辅助函数将组件中的 methods 映射为 store.commit
调用(需要在根节点注入 store
)。
1 | import { mapMutations } from 'vuex' |
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
1 | const store = createStore({ |
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。
实践中,我们会经常用到 ES2015 的参数解构来简化代码(特别是我们需要调用 commit
很多次的时候):
1 | actions: { |
分发 Action
Action 通过 store.dispatch
方法触发
1 | store.dispatch('increment') |
mutation 必须同步执行,Action 则不受约束,可以在 action 内部执行异步操作
1 | actions: { |
Actions 支持同样的载荷方式和对象方式进行分发
1 | // 以载荷形式分发 |
在组件中分发 Action
在组件中使用 this.$store.dispatch('xxx')
分发 action,或者使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
)
1 | import { mapActions } from 'vuex' |
组合 Action
store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise,并且 store.dispatch
仍旧返回 Promise
1 | actions: { |
1 | store.dispatch('actionA').then(() => { |
在另外一个 action 中也可以
1 | actions: { |
最后,如果我们利用 async / await,我们可以如下组合 action
1 | // 假设 getData() 和 getOtherData() 返回的是 Promise |
Module
当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
1 | const moduleA = { |
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象
1 | const moduleA = { |
对于模块内部的 action,局部状态通过 context.state
暴露出来,根节点状态则为 context.rootState
1 | const moduleA = { |
对于模块内部的 getter,根节点状态会作为第三个参数暴露出来
1 | const moduleA = { |
命名空间
默认情况下action,mutation和getter注册在全局命名空间,这样使得多个模块能够对同一个 action 或 mutation 作出响应。
必须注意,不要在不同的、无命名空间的模块中定义两个相同的 getter 从而导致错误。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。
当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
1 | const store = createStore({ |
启用了命名空间的 getter 和 action 会收到局部化的 getter
,dispatch
和 commit
。
换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced
属性后不需要修改模块内的代码。
在带命名空间的模块内访问全局内容(Global Assets)
如果你希望使用全局 state 和 getter,rootState
和 rootGetters
会作为第三和第四参数传入 getter,也会通过 context
对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true }
作为第三参数传给 dispatch
或 commit
即可。
1 | modules: { |
在带命名空间的模块注册全局 action
若需要在带命名空间的模块注册全局 action,你可添加 root: true
,并将这个 action 的定义放在函数 handler
中。
1 | { |
带命名空间的绑定函数
可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。
1 | computed: { |
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。
1 | computed: { |
你可以通过使用 createNamespacedHelpers
创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数
1 | import { createNamespacedHelpers } from 'vuex' |
指定空间名称
1 | // 通过插件的参数对象得到空间名称 |
模块动态注册
在 store 创建之后,你可以使用 store.registerModule
方法注册模块
1 | import { createStore } from 'vuex' |
之后就可以通过 store.state.myModule
和 store.state.nested.myModule
访问模块的状态。
模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。
你也可以使用 store.unregisterModule(moduleName)
来动态卸载模块,但不能使用此方法卸载静态模块(即创建 store 时声明的模块)
可以通过 store.hasModule(moduleName)
方法检查该模块是否已经被注册到 store。需要记住的是,嵌套模块应该以数组形式传递给 registerModule
和 hasModule
,而不是以路径字符串的形式传递给 module。
保留 state
在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState
选项将其归档:store.registerModule('a', module, { preserveState: true })
。
当你设置 preserveState: true
时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写。
模块重用
有时我们可能需要创建一个模块的多个实例
- 创建多个 store,他们公用同一个模块 (例如当
runInNewContext
选项是false
或'once'
时,为了在服务端渲染中避免有状态的单例) - 在一个 store 中多次注册同一个模块
如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。
1 | const MyReusableModule = { |