认识Vuex

学前准备


本文主要内容来源于官网,为Vuex的基础部分总结,部分案例也参考了互联网上其他分享知识的大佬。本手记结合官网内容也额外添加了自己的一些的理解,希望能给你带来一些参考价值,如果文章有理解不到位的地方,还请各位多批评指正!以下是本文中可能用到的参考资料:
点击进入vuex官方教程

点击进入Vue CLI官方教程

点击了解Vuex Action中的参数解构为什么那么写

点击了解Action和Promise中Promise详解

为什么使用Vuex


Vuex的官方解答:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

在vue开发的过程中,我们经常遇到一个状态可能会在多个组件之间使用,比如用户信息、头像、昵称等,我们修改了这些信息,那么整个应用中凡是使用了该信息的部分都应该更新。想让vue中所有组件都共享这些信息,那么就需要将这些信息进行集中管理,这个时候我们就需要用到Vuex

通过Vue CLI生成项目模板


日常开发中,我们大多都是用Vue CLI脚手架来生成一个vue项目,不太清楚脚手架怎么使用的可以移步Vue CLI官网自行查阅。在使用脚手架生成的项目时会让我们选择store,选择后会在页面中给我们生成store文件夹,自带初始化仓库的index.js,这就是最初的store,里面结构如下:

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules: {}
})

在模板中我们可以看到statemutationsactionsmodules这几个对象,而Vuex中几乎所有的操作都是围绕它们展开的,接下来我们就来逐一认识它们。

State


Vuex的核心仓库是store,这个store实例会被注入到所有的子组件里面,而所有子组件中公用的数据信息和状态就都存储在State里面。我们来看一个简单的小栗子。

export default new Vuex.Store({
  state: {
    count: 102
  }
})

vuex 中所有的状态管理都会定义在 state 中,我们在 state中定义了 count ,那么我们就得到了一个集中管理的状态 count ,所有的组件都可以直接访问到这个 count

<template>
  <div class="index">
    {{count}}
  </div>
</template>

<script>
  export default {
    computed: {
      count () {
        return this.$store.state.count
      }
    }
  }
</script>

因为根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到。通过计算属性,我们就可以在模板里面使用模板语法来调用count了,当然我们也可以在模板中的直接写入。有小伙伴会好奇为什么不能写在data里或其它地方吗,state写在计算属性中是因为模板文件需要响应仓库中state的变化,而state变化之后如果写在data中就不能及时响应渲染到页面中,而computed身为计算属性会监听函数下面值得变化然后进行重新渲染。

<template>
  <div class="index">
    {{this.$store.state.count}}
  </div>
</template>
  • mapState

如果我们在store中存储了很多个状态,而在当前模板页面中又要读取store里面大量状态,那么我们在当前页面采用computed中来return store里面的状态这种写法就会变得很冗余。Vuex中针对state给我们提供了它的语法糖mapState,我们可以通过mapState()方法直接获取state中存储的状态,如下栗子:

<template>
  <div class="index">
    {{count}}
    {{total}}
  </div>
</template>

<script>
  // 使用该语法糖我们需提前引入
  import { mapState } from 'vuex'
  export default {
    computed: {
      total () {
        return 10 + this.count
      },
      ...mapState(['count']) // 如需引入多个状态可写成...mapState(['count', 'name', 'six'])
    }
  }
</script>
// 102
// 112

Getter


有时候我们需要从 store 中的 state 中派生出一些状态,例如如果state中有一个数组,我们要将数组中所有大于10的数字挑选出来然后在给组件使用,栗子如下:

<template>
  <div class="index">
    {{modiyArr}}
  </div>
</template>

<script>
  export default {
    computed: {
      modiyArr () {
        return this.$store.state.list.filter(item => item > 10)
      }
    }
  }
</script>
// [20, 40, 50, 66, 77, 88]

我们在computed中完成了代码逻辑,同时也暴露出了一些问题,就是如果我们需要在多个组件中都完成这个数组的过滤,就需要在各个组件处都复制这段代码或者将其抽取到一个共享函数然后在多处导入它,无论哪一种方式都不是很理想。这个时候我们就可以使用vuex中的getter

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

<!-- 使用Getter的第一种用法 -->
<template>
  <div class="index">
    {{this.$store.getters.modiyArr}}
  </div>
</template>

<!-- 使用Getter的第二种用法 -->
<template>
  <div class="index">
    {{modiyArr}}
    {{getLength}}
  </div>
</template>

<script>
  export default {
    computed: {
      modiyArr () {
        return this.$store.getters.modiyArr
      },
      getLength () {
        return this.$store.getters.getLength
      }
    }
  }
</script>

// stoer.js
export default new Vuex.Store({
  state: {
    list: [1, 3, 4, 10, 20, 40, 50, 66, 77, 88]
  },
  getters: {
    // getters中的函数接收两个参数(state, getters)
    modiyArr(state) {
      return state.list.filter(item => item > 10)
    },
    getLength(state, getters) {
        return getters.modiyArr.length // 可以通过getters直接调用它的其它方法
    }
  }
})
// [20, 40, 50, 66, 77, 88] 

上面的栗子基本都是通过属性来访问的,使用getters也可以支持通过方法来访问,我们来看一个官网的小栗子

// 筛选state中的额todos,将todos数组中id为2的对象找出来
// store.js
const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => (id) => {
        return state.todos.find(todo  => todo.id === id)
    }
  }
})

<template>
  <div class="index">
    {{checkDoneTodos}}
  </div>
</template>

<script>
  import { mapState, mapGetter } from 'vuex'
  export default {
    computed: {
      checkDoneTodos () {
          return this.$store.getters.doneTodos(2)
      }
    }
  }
</script>
// { id: 2, text: '...', done: false }

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。getter在通过方法访问时,每次都会去进行调用,而不会缓存结果。

  • mapGetters

mapGetters也是Vuex帮我们封装好的语法糖,具体用法其实和mapState差不多。

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

当然...mapGetters本身也是支持对象类写法的

...mapGetters({
  // 把 `this.editArr` 映射为 `this.$store.getters.modiyArr`
  editArr: 'modiyArr',
  getLength: 'getLength'
})

Mutation


如果我们需要对store.js中的状态进行修改,我们是不能在组件里面直接修改的,因为组件里面直接修改仓库是不会进行全局响应的,这就违背了我们使用仓库的初衷。唯一的方法就是通过提交mutation来进行修改,这样仓库中与之对应的状态也会被修改进而影响全部组件的数据。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

<template>
  <div class="index">
    <button @click="add">增加</button>
    {{num}}
    <button @click="reduce">减少</button>
  </div>
</template>

<script>
  import { mapState, mapGetter } from 'vuex'
  export default {
    computed: {
      ...mapState(['num'])
    },
    methods: {
      add() {
        this.$store.commit('add')
      },
      reduce () {
        this.$store.commit('reduce')
      }
    }
  }
</script>

export default new Vuex.Store({
  state: {
    num: 10
  },
  mutations: {
    add (state) {
      // 变更状态
      state.num++
    },
    reduce (state) {
      state.num--
    }
  }
})

上面栗子中,我们在state中定义了num,然后通过mutations添加add方法和reduce方法去对num进行修改,在模板文件中我们定义了click事件来改变num的值,模板文件中的事件去响应store中的mutations主要是通过commit()来实现的。

  • 提交载荷(Payload)

你可以向store.commit传入额外的参数,即 mutation 的 载荷(payload),其实换种方式理解可能更容易,就是模板文件中的commit可以添加额外的参数,mutations中接收的时候接收一个形参payload,就代表模板文件中你传进来的参数。vuex官网更建议我们payload应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读,我们看下面的小栗子:

export default {
  methods: {
    add() {
      //this.$store.commit('add', 100)直接传递额外参数,不建议,更建议下一种对象的写法
      this.$store.commit('add', {
        addNum: 100
      })
    }
  }
}

export default new Vuex.Store({
  state: {
    num: 10
  },
  mutations: {
    add (state, payload) {
      state.num+=payload.addNum
    },
    reduce (state) {
      state.num--
    }
  }
})

也可以将所有参数写到一个对象里面,那么type就对应mutations中要执行的函数名

export default {
  methods: {
    add() {
      this.$store.commit({
        type: 'add',
        addNum: 100
      })
    }
  }
}
  • Mutation 需遵守 Vue 的响应规则

这个通俗点说就是你在开发过程中需要向state里面添加额外数据时,需要遵循响应准则。官方文档说既然 Vuexstore 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用Vue一样遵守一些注意事项: 1.最好提前在你的 store 中初始化好所有所需属性。 2.当需要在对象上添加新属性时,你应该使用Vue.set(obj, 'newProp', 123),或者以新对象替换老对象。例如,利用对象展开运算符我们可以这样写:

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

文字很枯燥,我们还是来看个小栗子

<template>
  <div class="index">
    <button @click="add">增加</button>
    {{num}}
    // add方法执行之后页面会立即响应这个新的状态
    <div class="new-num">{{this.$store.state.newNum || 0}}</div> 
  </div>
</template>

mutations: {
  add (state, payload) {
    state.num+=payload.addNum
    Vue.set(state, 'newNum', 1000) // 像仓库中的state新增加一个newNum状态,初始值为1000
    // state= {...state, newNum: 2000} 这个方法不管用了,用下面的replaceState()方法
    this.replaceState({...state, newNum: 2000})
  }
}
  • Mutation 必须是同步函数

下面这种写法必须避免(直接官方例子加持):

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

我们在模板文件中通过事件去操作mutations时,如果mutations中为异步函数,那么当 mutation 触发的时候,回调函数还没有被调用,因为我们不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

  • mapMutations

其实这几个语法糖的使用方法都差不多,这里就直接上官方栗子了。

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')`
    })
  }
}

Action


Action类似于mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。
  • Action 可以包含任意异步操作。前面说过mutation只能包含同步事务,所以在处理异步事务就需要Action,通过Action控制了异步这一过程,之后再去调用mutation里面的方法来改变状态。

先看官方的一个小栗子来认识它的基础语法:

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.statecontext.getters 来获取 stategetters。那为什么这里的参数不能直接是store本身呢,这就和Modules有关了,了解Modules之后就会发现store总仓库下面可能会有很多个不同的模块仓库,而每一个不同的模块仓库都有自己的Actionstatemutationgetter,如果使用store那么store.state拿到的就不会是当前模块的state,而context可以理解为当前模块的store,这样就不会引起冲突。

实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit 很多次的时候):

actions: {
  increment ({ commit }) { // 这里就是将context展开式写法 { commit } = context.commit
    commit('increment')
  }
}
  • 分发 Action

Action 通过 store.dispatch 方法触发:

store.dispatch('increment')

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

上面这一块内容基本来源于官网,感觉官网对于 Action 函数的传参,分发等基本用法都说的比较详细,在这里我们只要记住一点Action用来执行异步操作仓库状态的情况。我们还是来做个小栗子更直观了解吧

<template>
  <div class="index">
    <button @click="add">增加</button>
    {{num}}
  </div>
</template>

<script>
  import { mapState, mapGetter } from 'vuex'
  export default {
    computed: {
      ...mapState(['num'])
    },
    methods: {
      add() {
        this.$store.dispatch('delayAdd')
      }
    }
  }
</script>

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

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    num: 10
  },
  mutations: {
    add (state, addNum) {
      state.num += addNum
    }
  },
  actions: {
    delayAdd (context) {
      setTimeout(() => {
        context.commit('add', 100)
      }, 1000)
    }
  }
})

上面是一个Action中最基础的用法,因为Mutation中不能接受异步操作,所以我们先将要修改的东西放在Mutation中,然后在Action中等待1s之后去操作Mutation进行提交来更新数据。至于分发Action是我们在模板中通过 this.$store.dispatch('delayAdd')来执行,这样我们就完成了一个基础的Action闭环demo

  • Actions 支持同样的载荷方式和对象方式进行分发:
// 以载荷形式分发
store.dispatch('delayAdd', {
  addNum: 100
})

actions: {
  delayAdd (context, payload) {
    setTimeout(() => {
      context.commit('add', payload.addNum)
    }, 1000)
  }
}

// 以对象形式分发
store.dispatch({
  type: 'delayAdd',
  addNum: 100
})

这里copy一个官方购物车示例,涉及到调用异步 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)
   )
 }
}

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

  • mapActions

Action也有类似的语法糖,这里就不多赘述了,其实用法都是差不多的,直接官方栗子

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')`
    })
  }
}
  • Action结合Promise

Action 通常是异步的,那么如何知道 Action 什么时候结束呢?我们这里假设一种情况,模板上有一个数据,同时有这个数据当前的状态说明,我们如果通过Action去操作这个数据,那么数据改变之后我们希望这个数据的当前状态说明就马上更新。举个小栗子

<template>
  <div class="index">
    <div class="index-name">姓名:{{userinfo.name}}</div>
    <div class="index-six">性别:{{userinfo.six}}</div>
    <div class="number-hint">状态:{{defalutText}}</div>
    <div class="click-btn" @click.stop="handleClickChange">
      <button>{{buttonText}}</button>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetter, mapAction } from "vuex";
export default {
  data () {
    return {
      defalutText: "信息暂未修改" ,
      buttonText: "点击改变信息",
      time: 5
    }
  },
  computed: {
    ...mapState(["userinfo"]),
  },
  methods: {
    handleClickChange () {
      this.changeButton()
      this.$store.dispatch({
        type: 'changeInfo',
        name: '李四',
        six: '女',
        time: this.time
      }).then(res => {
        console.log(res) // 执行完毕,可以开始改变状态啦
        this.defalutText = '信息更新成功'
      })
    },
    changeButton () {
      this.buttonText = this.time + 's后信息将被改变'
      let t = setInterval(() => {
        this.time--
        if (this.time != 0) {
          this.buttonText = this.time + 's后信息将被改变'
        } else {
            this.buttonText = '成功改变啦'
            clearInterval(t)
        }
      }, 1000)
    },
  },
};
</script>

// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userinfo: {
      name: '张三',
      six: '男'
    }
  },
  mutations: {
    changeInfo (state, payload) {
      state.userinfo.name = payload.name
      state.userinfo.six = payload.six
    }
  },
  actions: {
    // 这里直接用参数解构的写法
    changeInfo ({commit, state}, payload) { // 接收context(解构写法)和载荷payload
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          commit('changeInfo', {
            name: payload.name,
            six: payload.six
          })
          resolve("执行完毕,可以开始改变状态啦") // 成功之后即可直接在then的回调中接收
        }, payload.time * 1000)
      })
    }
  }
})

上面的代码基本就是先展示state中存好的userinfo信息,点击按钮之后会有一个time的倒计时来告诉你几秒之后改变数据,改变之后再将文字改成"成功改变啦"。

其实上面这段代码是有小瑕疵的,因为我们在分发actionchangeInfo函数中给的是一个定时器,没有任何延迟信息,所以5s之后返回来的参数和我们在changeButton()中写好的改变button状态的文字能正好对应上。而日常开发中通常这里会写请求函数到后端然后等待返回值,这个过程如果耽误1s那么们changeButton()中就会出现问题,它会优先执行this.buttonText = '成功改变啦'

而实际因为请求有时间延迟,可能多出1s就会出现文字改变而上面的信息并未更新。这里特意留给我们自己思考,如何完善这个小bug,其实答案很简单,就是在then()的回调中去处理这段逻辑,具体的实现可以自行去了解哦,对Promise不太了解的可以点击了解Promise

  • 组合Action
利用Promise来实现组和Action

我们先直接上官网的demo小栗子,通过栗子看更明白

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

其实上面的这个小栗子在我们上一块 Action结合Promise 中就说的很明白了,它就是想告诉我们通过Promise.resolve()之后我们就知道这个时候actionA就执行完毕了,我们就可以通过链式调用的then()方法来走下一个流程啦。看官方给的actionB

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

vue模板中调用

store.dispatch('actionA').then((res) => {
  // ... 这里执行actionA中异步结束之后的程序,可以接受res为actionA中resolve("将返回结果返回出去")
})
利用 async / await来实现组和Action

asyncawait是ES7中推出的新语法,直接看官方的栗子,简单明了

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // 等待 actionA 完成
    commit('gotOtherData', await getOtherData())
  }
}
一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

Module


由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的statemutationactiongetter、甚至是嵌套子模块——从上至下进行同样方式的分割,我们先来做个简单的栗子,这里我们就不直接在index.js里面新建module了,因为实际开发中我们也是将模块单独拎出来,这样更有利于代码维护。如下栗子:

// moduleA.js
const moduleA = {
  state: {
    userInfo: {
      name: 'alen',
      age: 20
    }
  },
  mutations: { ... },
  getters: { ... },
  actions: { ... }
}
export default moduleA

// moduleB.js
const moduleB = {
  state: {
    userInfo: {
      name: 'nemo',
      age: 32
    }
  },
  mutations: { ... },
  getters: { ... },
  actions: { ... }
}
export default moduleB

store仓库的跟文件index.js中引入模块A和模块B

import Vue from 'vue'
import Vuex from 'vuex'
import moduleA from './modules/moduleA'
import moduleB from './modules/moduleB'

Vue.use(Vuex)

export default new Vuex.Store({
  state: { ... },
  modules: {
    ma: moduleA,
    mb: moduleB
  }
})

上面的代码就让我们轻松完成了模块的新建和引入,如果我们想在组件里访问模块A中的name

<template>
  <div class="index">
    <div class="modulea-name">{{this.$store.state.ma.userInfo.name}}</div> // alen
    <div class="moduleb-name">{{this.$store.state.mb.userInfo.name}}</div> // nemo
  </div>
</template>

上面的代码我们不难猜出,起始mambstate都是挂载在根节点的state下面。所以我们在组件中直接访问模块中的state需要先去访问根节点的state然后在加上模块名及对应的参数。

  • 模块的局部状态

对于模块内部的 mutationgetter,接收的第一个参数是模块的局部状态对象。直接官方栗子加持:

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

同样,对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

const moduleA = {
  // ...
  actions: {
    // 这里用的解构写法,实际上是context.state, context.commit, context.rootState
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

对于模块内部的 getter ,根节点状态会作为第三个参数暴露出来:

const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

这里我们直接举一个组件操作模块B进行mutation提交的小栗子

<template>
  <div class="index">
    <div class="moduleb-name">{{this.$store.state.mb.userInfo.name}}</div>
    <div class="moduleb-age">{{this.$store.state.mb.userInfo.age}}</div>
    <button @click="handleClickChangeAge(60)">修改年龄</button>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  methods: {
    handleClickChangeAge(newAge) {
      this.$store.commit({
        type: 'editAge',
        age: newAge
      })
    }
  }
}
</script>

// moduleB.js
const moduleB = {
  state: {
    userInfo: {
      name: 'nemo',
      age: 32,
      hobby: ['football', 'basketball', 'badminton', 'volleyball']
    }
  },
  mutations: {
    editAge (state, payload) {
      state.userInfo.age = payload.age
    }
  }
}

export default moduleB

看上面的代码会发现我们通过 commit 提交模块B里面的内容并没有使用 mb 这个模块名,而是直接全局提交就能进行修改,我们再来看看 getters 是不是也是直接可以全局提交修改。

<template>
  <div class="index">
    <div class="moduleb-hobby">
      <div v-for="(item, index) in hobby" :key="index">{{item}}</div>
    </div>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {ddd
  computed: {
    modeulaName () {
      console.log(this.$store.state.ma.userInfo.name)
    },
    filteHobby () {
      console.log(this.$store.getters.filteHobby)
      return this.$store.getters.filteHobby
    }
    // 使用语法糖来写
    // ...mapGetters({
      // hobby: 'filteHobby'
    // })
  }
};
</script>

const moduleB = {
  state: {
    userInfo: {
      name: 'nemo',
      age: 32,
      hobby: ['football', 'basketball', 'badminton', 'volleyball']
    }
  },
  getters: {
    filteHobby (state, getters, rootState) {
      return state.userInfo.hobby.filter(item => item != 'football')
    }
  }
}
export default moduleB

果然 getters 也可以直接通过 this.$store.getters 来操作,而不需要再加上它所在的模块名来进行调用,这里我们来看看官方是这样说的:

默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。
  • 命名空间

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名。直接来看小栗子:

// 新建模块C
const moduleC = {
  namespaced: true,
  state: {
    userInfo: {
      name: 'kity',
      age: 10,
      list: [1, 2, 3, 4]
    }
  },
  getters: {
    filterList (state) {
      return state.userInfo.list.filter(item => item != 1)
    }
  },
  mutations: {
    changeAge (state, payload) {
      state.userInfo.age = payload.age
    }
  },
  actions: {...}
}
export default moduleC

// index.js中导入
import moduleC from './modules/moduleC'
export default new Vuex.Store({
  modules: {
    mc: moduleC
  }
})

// 模板文件
<template>
  <div class="index">
    <div class="modulec-name">{{this.$store.state.mc.userInfo.name}}</div>
    <div class="modulec-age">{{this.$store.state.mc.userInfo.age}}</div>
    <div class="modulec-list">
      <div v-for="(item, index) in list" :key="index">{{item}}</div>
    </div>
    <button @click="handleClickChangeAge(20)">修改年龄</button>
  </div>
</template>

通过 commit 提交 moduleC 中的 changeAge() ,这时候的写法如下:

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  methods: {
    handleClickChangeAge(newAge) {
      this.$store.commit('mc/changeAge',{
        age: newAge
      })
      this.$store.commit({
        type: 'mc/changeAge',
        age: newAge
      })
    }
  }
};
</script>

通过 getters 获取 moduleC 中的 filterList(),一种是返回值直接通过 this.$store 去写,一种是语法糖的写法

<script>
import { mapState, mapGetters, mapActions } from "vuex";
export default {
  computed: {
    list () {
      return this.$store.getters['mc/filterList']
    },
    // ...mapGetters({
    //   list: 'mc/filterList'
    // })
  }
};
</script>

当然模块里面再嵌套模块也可以,路径要不要多走一层主要看你的 namespaced: true 有没有声明,这里贴一下官方的代码:

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

启用了命名空间的 getteraction 会收到局部化的 getterdispatchcommit。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

  • 在带命名空间的模块内访问全局内容

如果你希望使用全局 stategetterrootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。这里直接官方栗子加持

modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}
  • 在带命名空间的模块注册全局 action

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction') // 通过dispatch直接调用不需要加上命名空间
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: { // 全局的action
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}
  • 在模块里面使用辅助函数mapState、mapGetters、mapMutations和mapActions

由于存在命名空间,在组件里面采用上面的写法会出现问题,这里要想使用辅助函数来映射模块里面的东西需要指定空间名称来告诉辅助函数应该去哪儿找这些。 这儿我以上面我的C模块为例,首先对于 mapSatate 函数可以这样玩,我在全局的 modules 里面声明了 mc,那我的空间名称就是 mc

computed: {
  ...mapState('mc', ['name', 'desc']) // 这里模块里面要使用辅助函数的话要多传一个参数才行
}

然后在模版里面写 namedesc 即可,或者可以这样:

computed: {
  ...mapState('mc', {
    name(state) {
      return state.name;
    },
    desc(state) {
      return state.desc;
    }
  })
}

mapActionsmapMutationsmapGetter都可以向上面一样类似写法,这里我们写一个mapMutations的栗子参考

<script>
import { mapState, mapGetters, mapActions, mapMutations } from "vuex";
export default {
  methods: {
    handleClickChangeAge(newAge) {
      // 通过commit提交的写法
      // this.$store.commit({
      //   type: 'mc/changeAge',
      //   age: newAge
      // })
      // 使用语法糖的写法
      this.changeAge({
        age: newAge
      })
      // 当...mapMutations中第二个参数使用对象写法时,this后面接的函数名应该是该对象的键
      this.editAge({
        age: newAge
      })
    },
    // 语法糖中第一个参数是对应的路径,第二个参数为数组时的写法
    ...mapMutations('mc', ['changeAge'])
    // 第二个参数为对象时的写法
    ...mapMutations('mc', {
      editAge: 'changeAge' // 特意区分了键和值,值代表mutations中的函数,键代表了模板中this调用的函数
    })
  }
};
</script>

如果你确实不想在每个辅助函数里写空间名称,Vuex 也提供了其它办法,使用createNamespacedHelpers 创建基于某个命名空间辅助函数,它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex';
const { mapState, mapMutations } = createNamespacedHelpers('mc');

这样你在写辅助函数的时候就不需要单独指定空间名称了。 其它类似,就不再赘述了!其实 vuex 官网中对于 Module 这个板块还有几个知识点,只是对于了解基础的话过多的深入可能还会影响自己的消化进度,如果当我们做一个项目庞大到需要建立很多个模块,然后模块中又进行嵌入,那么相信我们对 vuex 已经基本都了解了,到时候再去查阅相关的进阶资料也很容易理解。

结语


vuex 的几个核心概念的基本认识就都在这里了,本文也主要是参考了官网的文档进行归纳总结。当然本篇相当于基础入门篇,实际开发中使用 vuex 肯定远远比这个复杂,但是万丈高楼平地起,希望对大家有所帮助,至于其他进阶内容大家有兴趣进官网浏览,也可查阅相关的资料进行学习。如果文章有理解不到位的地方,还请各位多批评指正!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341