Vuex 状态管理工具

Vuex是什么?

Vuex 是一个专为 Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel调试、状态快照导入导出等高级调试功能。

反正我是懵懵的,没太看懂他有什么用。 (~ ̄▽ ̄)~

什么是“状态管理模式”?

让我们从一个简单的 Vue 计数应用开始:

newVue({// statedata () {return{count:0}      },// viewtemplate: `

{{count}}
`,// actionsmethods: {    increment () {this.count++    }  } })

1

2

3

4

5

6

7

8

9

10

11

12

13

14

1

2

3

4

5

6

7

8

9

10

11

12

13

14

这个状态自管理应用包含以下几个部分:

state,驱动应用的数据源; view,以声明方式将state映射到视图; actions,响应在view上的用户输入导致的状态变化。

我的理解是 state状态就是数据源,通常用data,view是视图层不用说,通常用template,action就是方法进行一些数据操作,通常用methods。

当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:

-多个视图依赖于同一状态。

-来自不同视图的行为需要变更同一状态。

应该是 很多视图层文件都是同一个数据来源,不同视图的操作需要改为同一个数据源。

对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。

因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者出发行为!

另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,我们的代码将会变得更结构化且易维护。

这就是 Vuex 背后的基本思想,借鉴了 Flux、Redux、和 The Elm Architecture。与其他模式不同的是,Vuex

是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。

也就是遇到上述问题怎么办呢,Vuex将数据源打包了,然后要操作的时候,都从这个数据源来操作,不会有那种继承父组件,多层组件嵌套导致不利于维护的情况。

什么情况下我应该使用 Vuex?

虽然 Vuex 可以帮助我们管理共享状态,但也附带了更多的概念和框架。这需要对短期和长期效益进行权衡。

如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的

global event bus

就足够您所需了。但是,如果您需要构建是一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用

Redux 的作者 Dan Abramov 的话说就是:

Flux架构就像眼镜:您自会知道什么时候需要它。

其实我真的觉得这段话说了等于没说。。。 ㄟ( ▔, ▔ )ㄏ

每一个 Vuex 应用的核心就是 store(仓库)。”store” 基本上就是一个容器,它包含着你的应用中大部分的状态(即state)。Vuex 和单纯的全局对象有以下两点不同:

1.Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store中的状态发生变化,那么相应的组件也会相应地得到高效更新。

2.你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交mutations。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

1大概也就是数据绑定,同步更新数据,2不是很懂,mutations是什么东西?

最简单的 Store

提示:我们将在后续的文档示例代码中使用 ES2015 语法。如果你还没能掌握 ES2015,你得抓紧了!

安装 Vuex 之后,让我们来创建一个 store。创建过程直截了当——仅需要提供一个初始 state 对象和一些 mutations:

// 如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex)const store =newVuex.Store({  state: {count:0},  mutations: {    increment (state) {      state.count++    }  }})

1

2

3

4

5

6

7

8

9

10

11

12

1

2

3

4

5

6

7

8

9

10

11

12

现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:

store.commit('increment')console.log(store.state.count)// -> 1

1

2

3

1

2

3

再次强调,我们通过提交 mutation 的方式,而非直接改变

store.state.count,是因为我们想要更明确地追踪到状态的变化。这个简单的约定能够让你的意图更加明显,这样你在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也让我们有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,我们甚至可以实现如时间穿梭般的调试体验。

由于 store 中的状态是响应式的,在组件中调用 store 中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的methods 中提交 mutations。

这是一个最基本的 Vuex 记数应用示例。

接下来,我们将会更深入地探讨一些核心概念。让我们先从 State 概念开始。

反正每次对数据对象进行操作的时候,都需要进行commit,好处就是他上面说的,但是现在不在项目中,肯定体会不到好处,就像redux作者说的“您自会知道什么时候需要它。”

state

单一状态树

Vuex 使用 单一状态树 ——

是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个『唯一数据源(SSOT)』而存在。这也意味着,每个应用将仅仅包含一个 store

实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

单状态树和模块化并不冲突 —— 在后面的章节里我们会讨论如何将状态和状态变更事件分布到各个子模块中。

每个应用将仅仅包含一个 store

在 Vue 组件中获得 Vuex 状态

那么我们如何在 Vue 组件中展示状态呢?由于 Vuex 的状态存储是响应式的,从 store

实例中读取状态最简单的方法就是在计算属性中返回某个状态:

// 创建一个 Counter 组件const Counter = {  template: `

{{count}}
`,  computed: {count() {returnstore.state.count}  }}

1

2

3

4

5

6

7

8

9

1

2

3

4

5

6

7

8

9

每当 store.state.count 变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。

然而,这种模式导致组件依赖的全局状态单例。在模块化的构建系统中,在每个需要使用 state

的组件中需要频繁地导入,并且在测试组件时需要模拟状态。

Vuex 通过 store 选项,提供了一种机制将状态从根组件『注入』到每一个子组件中(需调用 Vue.use(Vuex)):

constapp =newVue({  el:'#app',// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件store,  components: { Counter },  template: `    `})

1

2

3

4

5

6

7

8

9

10

11

1

2

3

4

5

6

7

8

9

10

11

通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store访问到。让我们更新下 Counter 的实现:

const Counter = {  template: `

{{count}}
`,  computed: {count() {returnthis.$store.state.count}  }}

1

2

3

4

5

6

7

8

1

2

3

4

5

6

7

8

通过一种类似虚拟主机的机制,将store注入到每个子组件,然后子组件就能操作store了,还是有点像继承。

mapState 辅助函数

当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState

辅助函数帮助我们生成计算属性

这个我看的不是很懂,只知道他是辅助计算属性的。

mapState函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符(现处于 ECMASCript 提案 stage-3 阶段),我们可以极大地简化写法。

上面的没懂,那这个等等再说。

Getters

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:

computed:{  doneTodosCount () {    return this.$store.state.todos.filter(todo => todo.done).length}}

1

2

3

4

5

1

2

3

4

5

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它 —— 无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义『getters』(可以认为是 store 的计算属性)。Getters 接受 state 作为其第一个参数:

conststore =newVuex.Store({  state: {    todos: [      { id:1, text:'...', done:true},      { id:2, text:'...', done:false}    ]  },  getters: {    doneTodos: state => {returnstate.todos.filter(todo => todo.done)    }  }})

1

2

3

4

5

6

7

8

9

10

11

12

13

1

2

3

4

5

6

7

8

9

10

11

12

13

Getters 会暴露为 store.getters 对象:

store.getters.doneTodos // -> [{ id:1, text:'...',done:true}]

1

1

Getters 也可以接受其他 getters 作为第二个参数:

getters: {  //...doneTodosCount: (state, getters) => {returngetters.doneTodos.length  }}store.getters.doneTodosCount // ->1

1

2

3

4

5

6

7

1

2

3

4

5

6

7

我们可以很容易地在任何组件中使用它:

computed:{  doneTodosCount () {    return this.$store.getters.doneTodosCount}}

1

2

3

4

5

1

2

3

4

5

getters可以认为是 store 的计算属性,也就是多次调用,所以将其封装成getters,方便调用。

mapGetters 辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getters 映射到局部计算属性:

import { mapGetters } from'vuex'export default {  //...computed: {  // 使用对象展开运算符将 getters 混入 computed 对象中    ...mapGetters(['doneTodosCount','anotherGetter',      //...])  }}

1

2

3

4

5

6

7

8

9

10

11

12

13

1

2

3

4

5

6

7

8

9

10

11

12

13

如果你想将一个 getter 属性另取一个名字,使用对象形式:

mapGetters({  // 映射 this.doneCount 为 store.getters.doneTodosCountdoneCount:'doneTodosCount'})

1

2

3

4

1

2

3

4

和上面的state一样,没太明白。不着急。

Mutations

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutations 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store =newVuex.Store({  state: {count:1},  mutations: {    increment (state) {// 变更状态state.count++    }  }})

1

2

3

4

5

6

7

8

9

10

11

1

2

3

4

5

6

7

8

9

10

11

你不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法:

store.commit('increment')

1

1

我到现在还是没有明白 mutations是干什么的,具体有什么用。只知道要操作state就是要commit mutations。

提交载荷(Payload)

你可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):

//...mutations: {  increment (state, n) {    state.count += n  }}store.commit('increment',10)

1

2

3

4

5

6

7

1

2

3

4

5

6

7

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

//...mutations: {  increment (state, payload) {    state.count += payload.amount  }}store.commit('increment', {  amount:10})

1

2

3

4

5

6

7

8

9

1

2

3

4

5

6

7

8

9

不知道载荷用的多不多,感觉还挺多的,荷载就是 commit额外的参数。

对象风格的提交方式

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

store.commit({type:'increment',  amount:10})

1

2

3

4

1

2

3

4

当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:

mutations:{  increment (state, payload) {    state.count+= payload.amount}}

1

2

3

4

5

1

2

3

4

5

感觉这种风格更适用于vue,更像vue的写法

Mutations 需遵守 Vue 的响应规则

既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

最好提前在你的 store 中初始化好所有所需属性。

当需要在对象上添加新属性时,你应该使用Vue.set(obj, 'newProp', 123),或者 -

以新对象替换老对象。例如,利用 stage-3 的对象展开运算符我们可以这样写:

state.obj= { ...state.obj, newProp:123}

1

1

规则很重要,我愣是挤出了这几个没用的字。 Σ( ° △ °|||)︴

使用常量替代 Mutation 事件类型

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:

// mutation-types.jsexport const SOME_MUTATION ='SOME_MUTATION'// store.jsimport Vuex from'vuex'import { SOME_MUTATION } from'./mutation-types'const store = new Vuex.Store({  state: {...},  mutations: {    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名    [SOME_MUTATION] (state) {      // mutate state    }  }})

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

用不用常量取决于你 —— 在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。

什么可不可以这样做,这个到底是干嘛的啊 ヾ(。`Д´。),难道是方便改事件名?

mutation 必须是同步函数

一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:

mutations:{  someMutation (state) {    api.callAsyncMethod(() => {      state.count++    })  }}

1

2

3

4

5

6

7

1

2

3

4

5

6

7

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用 —— 实质上任何在回调函数中进行的的状态的改变都是不可追踪的。

这个如果不是同步函数的话,就显示不了回调函数的状态日志了,可以这样理解吗?

在组件中提交 Mutations

你可以在组件中使用this.$store.commit('xxx')提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为store.commit调用(需要在根节点注入 store)。

import { mapMutations } from'vuex'export default {  //...methods: {    ...mapMutations(['increment'// 映射 this.increment() 为 this.$store.commit('increment')    ]),    ...mapMutations({      add:'increment'// 映射 this.add() 为 this.$store.commit('increment')    })  }}

1

2

3

4

5

6

7

8

9

10

11

12

13

1

2

3

4

5

6

7

8

9

10

11

12

13

这个倒是能看懂

下一步:Actions

在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你能调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务:

store.commit('increment')// 任何由"increment"导致的状态变更都应该在此刻完成。

推荐阅读更多精彩内容