vuex学习笔记

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

安装

  • 直接下载/CDN引用
    基于NPM的CDN链接(最新版本):https://unpkg.com/vuex
    下载后需要在vue之后引入:

    1
    2
    <script src="/path/to/vue.js"></script>
    <script src="/path/to/vuex.js"></script>
  • NPM

    npm install vuex –save

    在一个模块化的打包系统中,您必须显式地通过 Vue.use()来安装 Vuex:

    1
    2
    3
    4
    import Vue from 'vue'
    import Vuex from 'vuex'
    Vue.use(Vuex)
  • 自己构建
    如果需要使用 dev 分支下的最新版本,可以直接从 GitHub 上克隆代码并自己构建

    git clone https://github.com/vuejs/vuex.git node_modules/vuex
    cd node_modules/vuex
    npm install
    npm run build

基本概念

每一个 Vuex 应用的核心就是store(仓库),store里面存储着大部分的state(状态)。
Vuex 和单纯的全局对象有以下两点不同:

  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  • 不能直接改变 store 中的状态。改变 store 中的状态的 唯一途径 就是显式地提交(commit) mutations。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

store

创建store,初始化state对象和一些mutation

1
2
3
4
5
6
7
8
9
10
11
12
// 如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})

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

1
2
3
4
5
6
7
// count状态
store.state.count
// 提交mutation
store.commit('increment')

核心概念

state

每个应用仅包含一个store(单一状态树),用一个对象包含了全部的应用层级状态。

  • 在vue组件中获取vuex状态
    从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 创建一个 Counter 组件
    const Counter = {
    template: `<div>{{ count }}</div>`,
    computed: {
    count () {
    return store.state.count
    }
    }
    }

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

    但是这种模式导致组件依赖的全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const app = new Vue({
    el: '#app',
    // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
    store,
    components: { Counter },
    template: `
    <div class="app">
    <counter></counter>
    </div>
    `
    })

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

    1
    2
    3
    4
    5
    6
    7
    8
    const Counter = {
    template: `<div>{{ count }}</div>`,
    computed: {
    count () {
    return this.$store.state.count
    }
    }
    }
  • mapState 辅助函数
    当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。 mapState 辅助函数可以帮助我们生成计算属性,精简代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 在单独构建的版本中辅助函数为 Vuex.mapState
    import { mapState } from 'vuex'
    export default {
    // ...
    computed: mapState({
    // 箭头函数可使代码更简练
    // 当计算属性名称和state的子节点名称相同时,也可以向mapState传递一个字符串数组
    // 如:'count'
    count: state => state.count,
    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',
    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
    return state.count + this.localCount
    }
    })
    }
  • 对象展开运算符
    mapState函数返回的是一个对象。对象展开运算符可以讲多个对象合并为一个,方便传递最终对象给computed属性

    1
    2
    3
    4
    5
    6
    7
    computed: {
    localComputed () { /* ... */ },
    // 使用对象展开运算符将此对象混入到外部对象中
    ...mapState({
    // ...
    })
    }

getters

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}
}
})

Getters 会暴露为store.getters对象:

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

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

1
2
3
4
5
6
7
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1

这样我们就能在任何组件中使用getters了:

1
2
3
4
5
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}

  • mapGetters辅助函数
    mapGetters 辅助函数仅仅是将 store 中的 getters 映射到局部计算属性:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { mapGetters } from 'vuex'
    export default {
    // ...
    computed: {
    // 使用对象展开运算符将 getters 混入 computed 对象中
    ...mapGetters([
    // 如果你想将一个 getter 属性另取一个名字,使用对象形式
    // 映射 this.doneCount 为 store.getters.doneTodosCount
    // doneCount: 'doneTodosCount'
    'doneTodosCount',
    'anotherGetter',
    // ...
    ])
    }
    }

mutations

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

1
2
3
4
5
6
7
8
9
10
11
12
const store = new Vuex.Store({
state: {
count: 1
},
// mutations更像是事件注册,无法直接调用mutation handler
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})

要唤醒一个 mutation handler,需要以相应的 type 调用 store.commit 方法:

1
store.commit('increment')

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

    1
    2
    3
    4
    5
    6
    7
    8
    // ...
    mutations: {
    increment (state, n) {
    state.count += n
    }
    }
    store.commit('increment', 10)

    当荷载为对象时,可以包含多个字段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // ...
    mutations: {
    increment (state, payload) {
    state.count += payload.amount
    }
    }
    store.commit('increment', {
    amount: 10
    })
  • 对象风格的提交方式
    提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

    1
    2
    3
    4
    store.commit({
    type: 'increment',
    amount: 10
    })

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

    1
    2
    3
    4
    5
    mutations: {
    increment (state, payload) {
    state.count += payload.amount
    }
    }
  • Mutations 需遵守 Vue 的响应规则
    mutaion需要遵循vue的响应式原则:
    1.最好提前在你的 store 中初始化好所有所需属性。
    2.当需要在对象上添加新属性时,你应该:

    • 使用 Vue.set(obj, ‘newProp’, 123), 或者
    • 以新对象替换老对象。例如,利用 stage-3 的对象展开运算符可以这样写:
      1
      state.obj = { ...state.obj, newProp: 123 }
  • mutation 必须是同步函数
    实例:

    1
    2
    3
    4
    5
    6
    7
    mutations: {
    someMutation (state) {
    api.callAsyncMethod(() => {
    state.count++
    })
    }
    }

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { mapMutations } from 'vuex'
    export default {
    // ...
    methods: {
    ...mapMutations([
    'increment' // 映射 this.increment() 为 this.$store.commit('increment')
    ]),
    ...mapMutations({
    add: 'increment' // 映射 this.add() 为 this.$store.commit('increment')
    })
    }
    }

actions

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作
    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const store = new Vuex.Store({
    state: {
    count: 0
    },
    mutations: {
    increment (state) {
    state.count++
    }
    },
    actions: {
    increment (context) {
    context.commit('increment')
    }
    }
    })

    Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

  • 分发Action
    Action 通过 store.dispatch 方法触发:

    1
    store.dispatch('increment')

    在 action 内部执行异步操作

    1
    2
    3
    4
    5
    6
    7
    actions: {
    incrementAsync ({ commit }) {
    setTimeout(() => {
    commit('increment')
    }, 1000)
    }
    }

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { mapActions } from 'vuex'
    export default {
    // ...
    methods: {
    ...mapActions([
    'increment' // 映射 this.increment() 为 this.$store.dispatch('increment')
    ]),
    ...mapActions({
    add: 'increment' // 映射 this.add() 为 this.$store.dispatch('increment')
    })
    }
    }
  • 组合 Actions
    详情参见(https://vuex.vuejs.org/zh-cn/actions.html)

modules

详情参见Modules

项目结构

vuex结构需要遵循一些规则:
1.应用层级的状态应该集中到单个 store 对象中。
2.提交 mutation 是更改状态的唯一方法,并且这个过程是同步的
3.异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation、和 getters 分割到单独的文件Modules

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块

参考