Vuex
Vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
Vuex 使用过程演示
以vue-cli3
新创建出来的项目为例,演示 Vuex 的使用过程。
创建项目:
1 | vue create vuex-test |
安装vuex
:
1 | npm i vuex -S |
进入项目的src/
下新建一个文件store/index.js
,并写入:
1 | // store/index.js |
进入main.js
注入store
,使所有vue
组件能够使用vuex
:
1 | // main.js |
现在我们可以从组件的方法提交一个变更:
1 | methods: { |
在组件模板中使用状态:
1 | {{ count }} |
state 状态的改变会触发 computed 的重新计算
核心概念
State
Vuex 使用单一状态树,每个应用将仅仅包含一个 store 实例
mapState
辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性,让你少按几次键:
1 | // 在单独构建的版本中辅助函数为 Vuex.mapState |
上面 mapState()参数内三个方式都是获取 count 的值。
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState
传一个字符串数组。
1 | computed: mapState([ |
对象展开运算符
mapState
函数返回的是一个对象。可使用对象展开运算符将此对象混入到外部对象中
1 | computed: { |
组件仍然保有局部状态
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
Getter
有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:
1 | computed: { |
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter 接受 state 作为其第一个参数:
1 | const store = new Vuex.Store({ |
通过属性访问
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
1 | store.getters.doneTodos; // -> [{ id: 1, text: '...', done: true }] |
Getter 也可以接受其他 getter 作为第二个参数:
1 | getters: { |
我们可以很容易地在任何组件中使用它:
1 | computed: { |
注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
1 | getters: { |
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
mapGetters
辅助函数
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
1 | import { mapGetters } from "vuex"; |
如果你想将一个 getter 属性另取一个名字,使用对象形式:
1 | ...mapGetters({ |
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 **回调函数 (handler)**。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
1 | const store = new Vuex.Store({ |
你不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment
的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:
1 | store.commit("increment"); |
提交载荷(Payload)
你可以向 store.commit
传入额外的参数,即 mutation 的 载荷(payload):
1 | // ... |
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
1 | // ... |
对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type
属性的对象:
1 | store.commit({ |
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:
1 | mutations: { |
Mutation 需遵守 Vue 的响应规则
既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
- 最好提前在你的 store 中初始化好所有所需属性。
- 当需要在对象上添加新属性时,你应该
使用
Vue.set(obj, 'newProp', 123)
, 或者以新对象替换老对象。例如,利用对象展开运算符我们可以这样写:
1
state.obj = { ...state.obj, newProp: 123 };
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:
1 | // mutation-types.js |
用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。
Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:
1 | mutations: { |
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
在组件中提交 Mutation (mapMutations
辅助函数)
你可以在组件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
辅助函数将组件中的 methods 映射为 store.commit
调用(需要在根节点注入 store
)。
1 | import { mapMutations } from "vuex"; |
下一步:Action
在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务:
1 | store.commit("increment"); |
为了处理异步操作,让我们来看一看 Action。
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
让我们来注册一个简单的 action:
1 | const store = new Vuex.Store({ |
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。当我们在之后介绍到 Modules 时,你就知道 context 对象为什么不是 store 实例本身了。
实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit
很多次的时候):
1 | actions: { |
分发 Action
Action 通过 store.dispatch
方法触发:
1 | store.dispatch("increment"); |
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
1 | actions: { |
Actions 支持同样的载荷方式和对象方式进行分发:
1 | // 以载荷形式分发 |
来看一个更加实际的购物车示例,涉及到调用异步 API 和分发多重 mutation:
1 | actions: { |
注意我们正在进行一系列的异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。
在组件中分发 Action (mapActions
辅助函数)
你在组件中使用 this.$store.dispatch('xxx')
分发 action,或者使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store
):
1 | import { mapActions } from "vuex"; |
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 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 |
一个
store.dispatch
在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
更多
更多内容查看官方文档:https://vuex.vuejs.org/zh/guide/modules.html