# Vuex 状态管理

参考文档 Vuex 是什么?- Vuex https://vuex.vuejs.org/zh/ 状态管理 - Vue.js https://cn.vuejs.org/v2/guide/state-management.html

# 前言

# vuex是什么?

Vuex是一个专门为Vue.js 应用开发的状态管理模式,有如下特点:

  • 采用集中式存储管理所有的组件状态
  • 制定相应的规则保证状态以一种可预测的方式发生变化
  • Vue官方调试工具 devtools extension集成了Vuex,提供了零配置的time-travel调试、状态快照导入导出等高级调试功能。

# 安装

# 直接下载/CDN引用

https://unpkg.com/vuex 这里除了可以用来引入,还可以查看源码,源码才1200行不到

上面的链接会一直指向 NPM 上发布的最新版本。您也可以通过 https://unpkg.com/vuex@2.0.0 方式指定特定的版本。

<!-- 在vue之后引用 -->
<script src="/path/to/vue.js"></script>
<script src="/path/to/vuex.js"></script>

# NPM

npm install vuex --save

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

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)
// 当使用全局 script 标签引用 Vuex 时,不需要以上安装过程

# 注意事项

Vuex 依赖 Promise 如果浏览器不支持Promise,可以使用一个 polyfill 的库,例如 es6-promise,你可以通过 下面的 CDN 将其引入, 然后 window.Promise 会自动可用。

<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>

如果npm打包工具构建,需要

npm install es6-promise --save

在使用Vuex之前,导入

import 'es6-promise/auto'

# 为什么需要使用Vuex状态管理?

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

  • 多个视图依赖同一状态,多层嵌套组件里传递参数会非常繁琐,且无法进行兄弟组件间的状态传递
  • 来自不同视图的行为需要变更同一状态,用下面的两种方法变更和同步状态的多份拷贝,会非常脆弱,通常会导致代码难以维护
    • 通过父子组件直接引用 this.$refs,this.$root, this.$parent, this.$children
    • 通过事件,props 和 this.$emit

为了解决上面的问题,就需要用到Vuex了,Vuex把组件的共享状态抽取出来,以一个全局单例模式管理,不管在树的哪个位置,任何组件都能获取状态或者触发行为,这样就易于维护了。

# 什么时候需要使用Vuex

  • 如果不打算开发大型单页面应用,使用Vuex可能会增加复杂度。
  • 如果应用够简单,最好不要使用Vuex, 简单的store模式就够了。

# 一个简单的store模式

下面的例子中,两个组件引入相同全局变量, commonData发生变化vmA和vmB都将更新相应的视图,子组件也可以通过this.$root.$data去访问对应的数据,但调试会有问题,应用中的任何部分,在任何数据改变后,都不会留下变更过的记录,为了解决这个问题,就可以采用一个简单的 store模式

const commonData = {}
const vmA = new Vue({
  data: commonData
})

const vmB = new Vue({
  data: commonData
})

store模式

  • 所有store中state的改变,都放置在store自身的action中去管理。当错误出现时,会有一个log记录bug之前发生了什么
  • 每个实例/组件依然可以拥有和管理自己的私有状态
var store = {
  debug: true,
  state: {
    message: 'Hello!',
  },
  setMessageAction(newValue) {
    if (this.debug) console.log('setMessageAction triggered with', newValue)
    this.state.message = newValue
  },
  clearMessageAction () {
    if (this.debug) console.log('clearMessageAction triggered')
    this.state.message = ''
  }
}

var vmA = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

var vmB = new Vue({
  data: {
    privateState: {},
    sharedState: store.state
  }
})

如果继续延伸约定,组件不允许直接修改属于store实例的state,而应该执行action来分发事件,通知store去改变,这样约定的好处是:

  • 可以记录store中发生的state改变
  • 继续扩展功能可以做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具,把这些功能都加上,慢慢完善优化,就是vuex的实现

# 全局对象也可以管理状态,为什么需要vuex?

Vuex和单纯的全局对象,有以下两点不同:

  • vuex不能直接改变store中的状态,改变store中的状态唯一的途径是显示的提交(commit) mutation。(为什么这样做?这样做 可以方便跟踪每一个状态的变化,可以实现一些工具来更好地了解应用的执行,方便debug)
  • vuex的状态存储是响应式的,当Vue组件从store中读取状态的时候,如果store中的状态发生变化,name相应的组件也会进行高效更新。

# 最简单的 Store

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

// 调用示例
store.commit('increment')
console.log(store.state.count) // -> 1

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

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

# vuex核心概念

// vuex 基本功能概览
const store = new Vuex.Store({

  // state 用来存储多个组件实例需要共用的状态
  state: { 
    count: 0
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },  

  // getters,从store.state中派生的状态,用于进行一些state值初步的计算或过滤。
  getters: { 
    doneTodosCount() {
      return this.$store.state.todos.filter(todo => todo.done).length
    }
  },

  // 为了方便调试,禁止直接修改state,mutations 用于更变state的操作,同步修改
  mutations: {
    increment (state) {
      state.count++
    }
  },

  // 将异步操作单独提出来,用actions来管理,异步操作完成,修改state也是通过mutations来变更状态
  actions: { 
    // 参数为context,结构出其commit属性, context.commit提交一个 mutation
    incrementAsync ({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  },

  // 对于非常复杂的应用,store对象会变得十分臃肿,modules属性用于将store分割为模块。
  modules: {
    a: moduleA,
    b: mouduleB
  }
})

// 每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
const moduleA = {
  state: { ... },
  mutations: { ... },
}
const moduleB = {
  state: { ... },
  mutations: { ... },
  actions: { ... }
}

store.state.a // => moduleA 的状态
store.state.b // => moduleB 的状态

# state属性

# 单一状态树

Vuex使用单一状态树,用一个对象就包含了全部的应用层级状态。每个应用仅包含一个store实例,好处:

  • 单一状态树可以直接定位任一特定状态片段
  • 在调试的过程中也可以轻易的取得整个应用状态的快照。

单状态树和模块化并不冲突,由于Vuex的状态存储是响应式的,从store实例中读取状态最简单的方法就是在计算属性中返回某个状态

// 没当store.state.count变化的时候,都会重新求取计算属性,并触发更新相关联的DOM
const Counter = {
  template: '<div>{{ count }}</div>',
  computed: {
    count () {
      return store.state.count
      // 将store的实例注入到所有的子组件中后 
      // return this.$store.state.count
    }
  }
}

// 新建Vue实例时,使用store属性,可以将store的实例注入到所有的子组件中
const app = new Vue({
  el: '#app',
  store,
  components: { Counter },
  template: `
    <div class="app">
      <couter></counter>
    </div>
  `
})

# mapState辅助函数

当组件需要多个状态的时候,状态声明为计算属性显得有点冗余,可以使用mapState辅助函数生成计算属性。

import { mapState } from 'vuex'

export default {
  // ... 
  // 一. mapState(对象)
  computed: mapState({
    // 1.参数值是一个箭头函数
    count: state => state.count,

    // 2.参数值是一个字符串, 等价于 state => state.count
    countAlias: 'count',

    // 3.参数值是一个常规函数,可以用this获取局部状态
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })

  // 二. mapState(数组)
  // 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
  computed: mapState(['count']) // 映射 this.count 为 store.state.count
}

# 对象展开运算符

computed: {
  localComputed () { /* ... */},

  // 使用对象扩展运算符将此对象混入到外部对象中
  // 对象的扩展运算符:https://www.yuque.com/guoqzuo/js_es6/rxu7ms#0d337474
  ...mapState({
    // ...
  })
}

# getters属性

当多个组件需要用到某个state属性计算或过滤后的数据,可以将使用getters属性

// 如果多个组件需要用到 doneTodosCount 这个属性,会比较繁琐
computed: {
  doneTodosCount () {
    return this.$store.state.todos.filter(todo => todo.done).length
  }
}

// 将 doneTodosCount 封装到 getters
// 类似于计算属性,只有当它的依赖值发生了改变才会被重新计算
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    // 暴露属性-1,通过属性访问时,**会缓存结果**
    // store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
    // 暴露属性-2:第二个参数为getters
    // store.getters.doneTodosCount // -> 1
    doneTodosCount: (state, getters) => {
      return getters.doneTodos.length
    }

    // 暴露方法, getter 在通过方法访问时,每次都会去进行调用,**不会缓存结果**
    // store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
    getTodoById: (state) => (id) => {
      return state.todos.filter(todo => todo.id === id)
    }
  }
})

# mapGetters 辅助函数

mapGetters 辅助函数可以将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex'

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

    // 2.使用对象作为参数
    ...mapGetters({
      // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
      doneCount: 'doneTodosCount'
    })
  }
}

# mutations属性

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation

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

// commit时传参 - 1.第二个参数为数字或字符串
// 使用方法:store.commit('increment', 10)
mutations: {
  increment (state, n) {
    state.count += n
  }
}

// commit传参 - 2.第二个参数为对象,且命名为payload(建议)
// 使用方法: store.commit('increment', { amount: 10 })
mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

// commit传参 3.参数只有一个Object参数,将mutation的方法名设置为type属性
// 实现方法可以不用变动payload为整个传入的对象
store.commit({
  type: 'increment',
  amount: 10
}) 

# mutation需要遵守Vue的响应规则

Vuex的store中的状态是响应式的,变更状态时,监视状态的Vue组件也会自动更新,Vuex中mutation需要与使用Vue一样遵守一些注意事项:

  • 最好提前在 store 中初始化好所有需要的属性
  • 当需要在对象上添加新属性时,应该:
    • 使用Vue.set(obj, 'newProp', 123) 或者
    • 以新对象替换老对象。例如,使用 对象展开运算符
    state.obj = { ...state.obj, newProp: 123 }
    

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

使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式,好处如下:

  • 可以使linter之类的工具发挥作用
  • 把常量放到单独的文件中,可以让其他开发人员对整个app包含的mutation一目了然
  • 在需要多人协作的大型项目中,这会很有帮助,是否使用可以取决于自己的喜好。
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'

// 实际使用
// ...
mutations: {
  // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
  [SOME_MUTATION] (state) {
    // mutate state
  }
}

# mutation必须是同步函数

// 在mutation中混合异步调用会导致程序很难调试
// 例如:调用了两个包含异步回调的mutation来改变状态,你不知道什么时候回调,不知道哪个先回调
// 在Vuex中mutation都是同步事务,如果需要异步操作,请使用actions属性
mutations: {
  someMutation (state) {
    api.callAsyncMethod(() => {
      state.count++
    })
  }
}

# 在组件中提交Mutation

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

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    // 1. mapMutations(数组)
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),

    // 2. mapMutatioins(对象)
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
}

# actions属性

action类似mutation,不同在于

  • action提交的是mutation,而不是直接变更状态
  • action可以包含任意异步操作
  • 组件或实例触发mutation使用this.$state.commit,触发action使用this.$state.dispatch
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    // context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters
    // 组件里使用方法 store.dispatch('increment')
    // 为什么这里是context,而不是store实例本身,主要是因为module
    increment (context) {
      context.commit('increment')
    }

    // 使用结构赋值的写法
    increment ({ commit }) {
      setTimeout(()=> {
        commit('increment')
      }, 2000)
    }
  }
})

// action 异步实例
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)
    )
  }
}

# 分发action

Action 通过 store.dispatch 方法触发

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

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


// 在组件中分发action
import { mapActions } from 'vuex'

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

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

    // 2.mapAction(对象)
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

# 组合 Action

组合多个异步的action

actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  },
  // ...
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

// 外部触发actionA
store.dispatch('actionA').then(() => {
  // ...
})

// 使用async/await改写
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}

# modules属性

我认为是重难点,刚看时有点懵,后面自己写demo再反复看文档才算理解了。文档很精简但确实功能都讲到了。

由于Vue使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store对象就有可能非常臃肿。为了解决这个问题,Vuex允许将store分割为module,每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块

# 基本示例

// 模块a
const moduleA = {
  state: { 
    aCount: 10  
  },
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

// 模块b
const moduleB = {
  state: { 
    bCount: 100,
  },
  mutations: { ... },
  actions: { ... }
}

// 根store
const store = new Vuex.Store({
  state: {
    count: 0
  },
  modules: {
    a: moduleA,
    b: moduleB
  }
})

// 组件内 this.$store.state -> { count: 0, a: { aCount: 10 }, b: { bCount: 100 } };
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

# 在组件实例中访问module

上面的例子中,通过store.state可以访问子模块的state,那子模块里的getters、mutations和actions是否和state一样呢?通过一个例子可以说明

属性 子模块相应属性名是否可以和根节点相关的属性名一致
state 可以, 所有state属性会根据层级保存到this.$store.state里
getters 不可以,会抛出Error: duplicate getter key,所有getters属性都会直接存到 this.、$store.getters,根节点有对应的属性后,子模块再有这个属性会被忽略。在子模块geeters的第二个参数getters参数也是this.$store.getters, 不像第一个参数state那样是局部的。
mutations 可以, store.commit('commonMutations'),会根据层级依次触发所有同名的mutations
actions 可以, store.dispatch('commonActions'),会根据层级依次触发所有同名的Actions
/*

测试 demo 目录结构:
├── store
│   ├── index.js        # 根级别的store
│   └── modules          
│       ├── cart.js     # 购物车模块
│       └── product.js  # 产品模块
└── main.js

*/

// store/index.js 根stote
import Vue from "vue";
import Vuex from "vuex";

import cart from "./modules/cart";
import products from "./modules/products";

Vue.use(Vuex);

console.log("cart", cart);
console.log("product", products);

export default new Vuex.Store({
  state: {
    count: 0,
    commonCount: "root count"
  },
  getters: {
    testCommonGetters() {
      return "root getters";
    },
    rootGetters(state) {
      return state.count + 50;
    }
  },
  mutations: {
    rootMutations(state) {
      state.count = 50;
    },
    testCommonMutations() {
      console.log("root mutations");
    }
  },
  actions: {
    testCommonActions() {
      console.log("root actions");
    },
    rootActions(context) {
      console.log("rootActions, console after 2s later");
      setTimeout(() => {
        console.log(context);
      }, 2000);
    }
  },
  modules: {
    cart,
    products
  }
});


// store/modules/cart.js 购物车模块
export default {
  state: {
    cardCount: 10,
    commonCount: "cart count"
  },
  getters: {
    testCommonGetters() {
      return "card getters";
    },
    cardGetters(state) {
      return state.cardCount * 2;
    }
  },
  mutations: {
    cardMutations(state) {
      state.cardCount = 99;
    },
    testCommonMutations() {
      console.log("card mutations");
    }
  },
  actions: {
    cardActions(context) {
      console.log("cardActions, console after 2s later");
      setTimeout(() => {
        console.log(context);
      }, 2000);
    },
    testCommonActions() {
      console.log("cart actions");
    }
  },
  modules: {
    subCardModule: {
      state: {
        subCardModuleCount: "subCardState"
      }
    }
  }
};


// store/modules/products.js 产品模块
export default {
  state: {
    productsCount: 100,
    commonCount: "product count"
  },
  getters: {
    testCommonGetters() {
      return "products getters";
    },
    productGetters(state) {
      return state.productsCount * 2;
    }
  },
  mutations: {
    productMutations(state) {
      state.productsCount = 999;
    },
    testCommonMutations() {
      console.log("product mutations");
    }
  },
  actions: {
    productsActions(context) {
      console.log("productsActions, console after 2s later");
      setTimeout(() => {
        console.log(context);
      }, 2000);
    },
    testCommonActions() {
      console.log("product actions");
    }
  }
};

/*

- this.$store.state 打印:
 {
  "count": 0,
  "commonCount": "root count",
  "cart": {
    "cardCount": 10,
    "commonCount": "cart count",
    "subCardModule": {
      "subCardModuleCount": "subCardState"
    }
  },
  "products": {
    "productsCount": 100,
    "commonCount": "product count"
  }
}

// Error [vuex] duplicate getter key: testCommonGetters
- this.$store.getters.productGetters 打印 200
- this.$store.getters.testCommonGetters 打印 root getters

- this.$store.commit("cardMutations");  // cardCount被改为99

- this.$store.commit("testCommonMutations") 打印:
root mutations
card mutations
product mutations

- this.$store.dispatch("cardActions"); 打印
cardActions, console after 2s later
...

- this.$store.dispatch("testCommonActions") 打印
root actions
cart actions
product actions


*/

# 模块的局部状态

  • 子模块内部的getters和mutations,接收的第一个参数为局部的 state (包含其子module的state,非根级)
    • (复习) mutations 第二参数为payload,store.commit时的传参
    • getters第二个参数为getters,非局部getters,而是全局的getters
    • getters第三个参数为rootState,根级别的state
  • 模块内部的 action,局部状态和通过 context.state 暴露出来, 根节点状态则为 context.rootState
    • (复习) context.commit提交一个 mutation,context.state 和 context.getters 用来获取 state 和 getters

这里可以思考为什么action需要使用context, 而不是像mutation那样用state作为参数 ?

  • mutations只需要用来变更状态,一个局部的state参数足够,第二个参数用于commit时的传值。如果需要使用rootState可以使用getters
  • actions里可以做的事情比较多,参数也多。且第二个参数需要留给dispatch传值用。commit、state、rootState,getters等参数有必要使用一个对象(context)来存储

另一个问题,子模块中getters和mutations为什么不直接使用this来获取,而是直接用context传呢?

  • Vuex的约束规则,所有的state变更都需要显式的调用commit,执行action需要使用dispatch,直接使用this会破坏约束,可能会导致不好追踪问题,不利于维护。
  • 子模块只能访问自身的getters和mutations,actions,而context传入的是全局的,可以访问其他模块的actions、mutations等方法。
const moduleA = {
  state: { count: 0 },
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },

  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  },

  // 模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
  incrementIfOddOnRootSum ({ state, commit, rootState }) {
    if ((state.count + rootState.count) % 2 === 1) {
      commit('increment')
    }
  }
}

# 命名空间

# 为什么需要有带命名空间的模块?
  • 默认情况下,模块内部的actions,mutations和getters是注册在全局命名空间的。多个模块能够对同一mutation或action作出响应。比如上面的例子中,子模块内部commit某个mutation,根store和其他模块中所有同名的mutation都会被触发
  • 封装性差,且子模块不能有和其他模块有重名的getters,否则获取的值会有问题或者导致其他模块获取的getters有问题。
# 带命名空间模块有什么好处?
  • 模块能有更好的封装性、复用性、且不会干扰外部
# 怎么让一个模块成为命名空间模块?
  • 为模块添加一个namespaced为true的属性
const moduleA = {
  namespaced: true,
  store: { ... }
  mutations: { ... }
} 
# 带命名空间的模块有哪些特性?
  • state和之前一样,没有任何影响
  • 所有的getters、mutations、actions都会自动根据模块注册的路径调整命名,命名空间模块的getter、mutations、actions 无法被外部直接访问,需要加上路径
// 如果模块名为 a,那么组件实例访问子模块内部的getters,mutations,actions方法如下:
this.$store.getters['a/somegetters']
this.$store.commit('a/somemutations')
this.$store.dispatch('a/someactions')
  • 命名空间模块 getter,dispatch 和 commit 会收缩到局部模块。比如:命名空间模块内部context.commit("testCommonMutations"),只触发当前模块的,不会触发外部的同名mutations
// 带命名空间模块内部的action中使用内部的action、geeters、mutations是不用加路径的
// 也就是非命名空间模块和命名空间模块的的切换,可以不用改动模块内部代码。
# 带命名空间的模块,怎么访问全局内容呢?
  • 对于getters来说,全局的state和全局的getters会做为函数的第3,4个参数
// 带命名空间的模块 foo 下面的 getters 
getters: {
  someGetter (state, getters, rootState, rootGetters) {
    getters.someOtherGetter // -> 'foo/someOtherGetter'
    rootGetters.someOtherGetter // -> 'someOtherGetter'
  },
}
  • 对于actions来说,context也暴露了rootGetters
  • mutations和actions,通过commit,dispatch的第三个参数加入 {root: true} 触发根级的mutation和action
// 带命名空间的模块 foo 下面的 actions
actions: {
  // 在这个模块中, dispatch 和 commit 也被局部化了
  // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
  someAction ({ dispatch, commit, getters, rootGetters }) {
    getters.someGetter // -> 'foo/someGetter'
    rootGetters.someGetter // -> 'someGetter'

    dispatch('someOtherAction') // -> 'foo/someOtherAction'

    // 第二个参数为null,就是不传值
    dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

    commit('someMutation') // -> 'foo/someMutation'
    commit('someMutation', null, { root: true }) // -> 'someMutation'
  }
}
  • 模块内部的actions可以注册到全局action,一般actions属性对应的是一个函数,注册到全局时,需要使用一个包含root和handler属性的对象,root设置为true,handler指定对应的处理函数
actions: {
  normalAction({ commit }) {
    commit('someMutation')
  },
  someAction: {
    root: true, // 将 someAction 注册到全局
    handler (namespacedContext, payload) { ... } // -> 'someAction'
  }
}

# 命名空间与mapState等辅助函数

// vue 组件或实例中 用辅助函数导入对应的变量,当模块层级比较深时,使用会很繁琐
computed: {
  ...mapState({
    a: state => state.moduleA.a,
    b: state => state.moduleA.b
  })
},
methods: {
  ...mapActions([
    'moduleA/foo', // -> this['moduleA/foo']()
    'moduleA/bar' // -> this['moduleA/bar']()
  ])
}

// 简化方法 - 1.使用辅助函数的第一个参数传模块路径,第二个参数传对应的数据
computed: {
  ...mapState('moduleA', { // 如果路径比较深,比如 some/nested/module
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('moduleA', [ // 模块路径也可以是更深层级的 some/nested/module
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

// 简化方法 - 2.使用 createNamespacedHelpers 创建基于某个命名空间辅助函数
import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

# 模块动态注册

store创建后,可以通过 store.registerModule 方法动态注册模块,如果是动态注册的模块,是可以通过store.unregisterModule(moduleName) 来动态卸载模块(非动态模块,无法用这个方法卸载)

// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

# 模块重用

const MyReusableModule = {
  state () {
    return {
      foo: 'bar'
    }
  },
  // mutation, action 和 getter 等等...
}

# 项目结构

Vuex并不限制代码结构,但需要准守一些规则:

  • 应用层级的状态应该集中到单个的对象中。
  • 提交mutation是改变状态的唯一方法,且过程是同步的。
  • 异步逻辑应该封装到action里面

如果store文件太大,可以将action、mutation和getter分割到单独的文件, 项目结构示例

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

# 插件

Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。

下面是使用内置的logger插件示例,会打印每次mutation的详细log。如果正在使用 vue-devtools,就不需要此插件。此插件仅限于开发时使用。

import Vue from "vue";
import Vuex from "vuex";
import createLogger from "vuex/dist/logger";

export default new Vuex.Store({
  plugins: [createLogger()], // 使用内置的logger插件
  state: {
    count: 0,
    commonCount: "root count"
  }
}

# 严格模式

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

const store = new Vuex.Store({
  // ...
  strict: true // 开启严格模式
})

不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变更——请确保在发布环境下关闭严格模式,以避免性能损失。

// 可以让构建工具来处理这种情况:
const store = new Vuex.Store({
  // ...
  strict: process.env.NODE_ENV !== 'production'
})

# store与表单关联处理

如果表单的v-model指向一个state,而state的改变只有显示的提交mutation,需要做一些处理

<input v-model="message">
<script>
  // ...
  computed: {
    message: {
      get () {
        return this.$store.state.message
      },
      set (value) {
        this.$store.commit('updateMessage', value)
      }
    }
  }
</script>

# 测试

待完善

# 实例

购物车示例 (opens new window)

上次更新: 2020/10/29 22:59:19