### 彪哥

爱好前端

- 基础

- 组件

- 过渡&动画

- 可复用性&组合

- 工具

- 规模化

- Vuex

  - Vuex
    - Vuex使用过程演示
    - 核心概念
      - State
      - mapState 辅助函数
      - 对象展开运算符
      - 组件仍然保有局部状态
      - Getter
      - 通过属性访问
      - 通过方法访问
      - mapGetters 辅助函数
      - Mutation
      - 提交载荷（Payload）
      - 对象风格的提交方式
      - Mutation 需遵守 Vue 的响应规则
      - 使用常量替代 Mutation 事件类型
      - Mutation 必须是同步函数
      - 在组件中提交 Mutation (mapMutations辅助函数)
      - 下一步：Action
      - Action
      - 分发 Action
      - 在组件中分发 Action （mapActions 辅助函数）
      - 组合 Action
    - 更多

- 其他

- 《Vue》笔记
- Vuex

heBody
2020-08-08
目录

# Vuex

# # Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。

## # Vuex使用过程演示

以`vue-cli3`新创建出来的项目为例，演示Vuex的使用过程。

创建项目:

    vue create vuex-test
    cd vuex-test
    npm run serve

1
2
3

安装`vuex`:

    npm i vuex -S

1

进入项目的`src/`下新建一个文件`store/index.js`，并写入：

    // store/index.js
    import Vue from 'vue'
    import Vuex from 'vuex'

    Vue.use(Vuex)

    const store = new Vuex.Store({ // 容器 （构造函数Store开头大写）
        state: { // 状态
            count: 0
        },
        mutations: { // 变化（使用mutations提交改变是为了方便追踪变化记录）
            increment (state){
                state.count++
            }
        }
    })
    export default store // 导出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

进入`main.js` 注入`store`，使所有`vue`组件能够使用`vuex` :

    // main.js
    import Vue from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'

    Vue.config.productionTip = false

    new Vue({
      router,
      store,
      render: h => h(App)
    }).$mount('#app')

1
2
3
4
5
6
7
8
9
10
11
12
13

现在我们可以从组件的方法提交一个变更：

    methods: {
      increment() {
        this.$store.commit('increment') // .commit('<mutations里的事件名>')
        console.log(this.$store.state.count)
      }
    }

1
2
3
4
5
6

在组件模板中使用状态：

    {{ count }}

    computed: {
    	count() {
    		return this.$store.state.count
    	}
    }

1
2
3
4
5
6
7

> state状态的改变会触发computed的重新计算

## # 核心概念

### # State

Vuex 使用单一状态树，每个应用将仅仅包含一个 store 实例

#### # `mapState` 辅助函数

当一个组件需要获取多个状态的时候，将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题，我们可以使用 `mapState` 辅助函数帮助我们生成计算属性，让你少按几次键：

    // 在单独构建的版本中辅助函数为 Vuex.mapState
    import { mapState } from 'vuex'

    export default {
      // ...
      computed: mapState({
        // 箭头函数可使代码更简练
        count: state => state.count,

        // 传字符串参数 'count' 等同于 `state => state.count`
        countAlias: 'count',

        // 为了能够使用 `this` 获取局部状态，必须使用常规函数
        countPlusLocalState (state) {
          return state.count + this.localCount
        }
      })
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面mapState()参数内三个方式都是获取count的值。

当映射的计算属性的名称与 state 的子节点名称相同时，我们也可以给 `mapState` 传一个字符串数组。

    computed: mapState([
      // 映射 this.count 为 store.state.count
      'count'
    ])

1
2
3
4

#### # 对象展开运算符

`mapState` 函数返回的是一个对象。可使用对象展开运算符将此对象混入到外部对象中

    computed: {
      localComputed () { /* ... */ }, // 其他的计算属性
      // 使用对象展开运算符将此对象混入到外部对象中
      ...mapState({
        // ... 这里面和上面的获取方式是一样的
      })
    }

1
2
3
4
5
6
7

#### # 组件仍然保有局部状态

使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试，但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件，最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。

### # Getter

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

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

1
2
3
4
5

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

Vuex 允许我们在 store 中定义“getter”（可以认为是 store 的计算属性）。就像计算属性一样，getter 的返回值会根据它的依赖被缓存起来，且只有当它的依赖值发生了改变才会被重新计算。

Getter 接受 state 作为其第一个参数：

    const store = new Vuex.Store({
      state: {
        todos: [
          { id: 1, text: '...', done: true },
          { id: 2, text: '...', done: false }
        ]
      },
      getters: {
        doneTodos: state => {
          return state.todos.filter(todo => todo.done)
        }
      }
    })

1
2
3
4
5
6
7
8
9
10
11
12
13

#### # 通过属性访问

Getter 会暴露为 `store.getters` 对象，你可以以属性的形式访问这些值：

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

1

Getter 也可以接受其他 getter 作为第二个参数：

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

1
2
3
4
5
6
7

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

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

1
2
3
4
5

注意，getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

#### # 通过方法访问

你也可以通过让 getter 返回一个函数，来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。

    getters: {
      // ...
      getTodoById: (state) => (id) => {
        return state.todos.find(todo => todo.id === id)
      }
    }
    store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

1
2
3
4
5
6
7

注意，getter 在通过方法访问时，每次都会去进行调用，而不会缓存结果。

#### # `mapGetters` 辅助函数

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

    import { mapGetters } from 'vuex'

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

1
2
3
4
5
6
7
8
9
10
11
12
13

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

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

1
2
3
4

### # Mutation

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

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

1
2
3
4
5
6
7
8
9
10
11

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

    store.commit('increment')

1

#### # 提交载荷（Payload）

你可以向 `store.commit` 传入额外的参数，即 mutation 的 载荷（payload）：

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

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

#### # 对象风格的提交方式

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

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

1
2
3
4

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

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

1
2
3
4
5

#### # Mutation 需遵守 Vue 的响应规则

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

1. 最好提前在你的 store 中初始化好所有所需属性。
2. 当需要在对象上添加新属性时，你应该

- 使用 `Vue.set(obj, 'newProp', 123)`, 或者

- 以新对象替换老对象。例如，利用对象展开运算符
  (opens new window)我们可以这样写：

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

  1

#### # 使用常量替代 Mutation 事件类型

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

    // mutation-types.js
    export const SOME_MUTATION = 'SOME_MUTATION'
    // store.js
    import 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

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

#### # Mutation 必须是同步函数

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

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

1
2
3
4
5
6
7

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

#### # 在组件中提交 Mutation (`mapMutations`辅助函数)

你可以在组件中使用 `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` 也支持载荷：
          'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
        ]),
        ...mapMutations({
          add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
        })
      }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#### # 下一步：Action

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

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

1
2

为了处理异步操作，让我们来看一看 Action
(opens new window)。

### # Action

Action 类似于 mutation，不同在于：

- Action 提交的是 mutation，而不是直接变更状态。
- Action 可以包含任意异步操作。

让我们来注册一个简单的 action：

    const store = new Vuex.Store({
      state: {
        count: 0
      },
      mutations: {
        increment (state) {
          state.count++
        }
      },
      actions: {
        increment (context) {
          context.commit('increment')
        }
      }
    })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象，因此你可以调用 `context.commit` 提交一个 mutation，或者通过 `context.state` 和 `context.getters` 来获取 state 和 getters。当我们在之后介绍到 Modules
(opens new window) 时，你就知道 context 对象为什么不是 store 实例本身了。

实践中，我们会经常用到 ES2015 的 参数解构
(opens new window) 来简化代码（特别是我们需要调用 `commit` 很多次的时候）：

    actions: {
      increment ({ commit }) {
        commit('increment')
      }
    }

1
2
3
4
5

#### # 分发 Action

Action 通过 `store.dispatch` 方法触发：

    store.dispatch('increment')

1

乍一眼看上去感觉多此一举，我们直接分发 mutation 岂不更方便？实际上并非如此，还记得 mutation 必须同步执行这个限制么？Action 就不受约束！我们可以在 action 内部执行异步操作：

    actions: {
      incrementAsync ({ commit }) {
        setTimeout(() => {
          commit('increment')
        }, 1000)
      }
    }

1
2
3
4
5
6
7

Actions 支持同样的载荷方式和对象方式进行分发：

    // 以载荷形式分发
    store.dispatch('incrementAsync', {
      amount: 10
    })

    // 以对象形式分发
    store.dispatch({
      type: 'incrementAsync',
      amount: 10
    })

1
2
3
4
5
6
7
8
9
10

来看一个更加实际的购物车示例，涉及到调用异步 API 和分发多重 mutation：

    actions: {
      checkout ({ commit, state }, products) {
        // 把当前购物车的物品备份起来
        const savedCartItems = [...state.cart.added]
        // 发出结账请求，然后乐观地清空购物车
        commit(types.CHECKOUT_REQUEST)
        // 购物 API 接受一个成功回调和一个失败回调
        shop.buyProducts(
          products,
          // 成功操作
          () => commit(types.CHECKOUT_SUCCESS),
          // 失败操作
          () => commit(types.CHECKOUT_FAILURE, savedCartItems)
        )
      }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

注意我们正在进行一系列的异步操作，并且通过提交 mutation 来记录 action 产生的副作用（即状态变更）。

#### # 在组件中分发 Action （`mapActions` 辅助函数）

你在组件中使用 `this.$store.dispatch('xxx')` 分发 action，或者使用 `mapActions` 辅助函数将组件的 methods 映射为 `store.dispatch` 调用（需要先在根节点注入 `store`）：

    import { mapActions } from 'vuex'

    export default {
      // ...
      methods: {
        ...mapActions([
          'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

          // `mapActions` 也支持载荷：
          'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
        ]),
        ...mapActions({
          add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
        })
      }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

#### # 组合 Action

Action 通常是异步的，那么如何知道 action 什么时候结束呢？更重要的是，我们如何才能组合多个 action，以处理更加复杂的异步流程？

首先，你需要明白 `store.dispatch` 可以处理被触发的 action 的处理函数返回的 Promise，并且 `store.dispatch` 仍旧返回 Promise：

    actions: {
      actionA ({ commit }) {
        return new Promise((resolve, reject) => {
          setTimeout(() => {
            commit('someMutation')
            resolve()
          }, 1000)
        })
      }
    }

1
2
3
4
5
6
7
8
9
10

现在你可以：

    store.dispatch('actionA').then(() => {
      // ...
    })

1
2
3

在另外一个 action 中也可以：

    actions: {
      // ...
      actionB ({ dispatch, commit }) {
        return dispatch('actionA').then(() => {
          commit('someOtherMutation')
        })
      }
    }

1
2
3
4
5
6
7
8

最后，如果我们利用 async / await
(opens new window)，我们可以如下组合 action：

    // 假设 getData() 和 getOtherData() 返回的是 Promise

    actions: {
      async actionA ({ commit }) {
        commit('gotData', await getData())
      },
      async actionB ({ dispatch, commit }) {
        await dispatch('actionA') // 等待 actionA 完成
        commit('gotOtherData', await getOtherData())
      }
    }

1
2
3
4
5
6
7
8
9
10
11

> 一个 `store.dispatch` 在不同模块中可以触发多个 action 函数。在这种情况下，只有当所有触发函数完成后，返回的 Promise 才会执行。

## # 更多

更多内容查看官方文档：https://vuex.vuejs.org/zh/guide/modules.html
(opens new window)

编辑
(opens new window)
上次更新: 2022/12/14, 19:36:42
路由懒加载
Vue中的防抖函数封装和使用

← 路由懒加载 Vue中的防抖函数封装和使用→

最近更新

  01
Git修改分支名
08-11

  02
CSS给table的tbody添加滚动条
06-29

  03
我做了一个手写春联小网页，祝大家虎年暴富 原创
01-28

更多文章>

Copyright © 2016-2022 Hesb | MIT License

- 跟随系统
- 浅色模式
- 深色模式
- 阅读模式