Vuex的五个核心概念—state/getter/mutation/action/module

1. State

Vuex 使用单一状态树——用一个对象包含全部的应用层级状态。至此它就是“唯一数据源 (SSOT)”。这也意味着,每个应用将仅仅包含一个 store 实例。

组件中获得 Vuex 状态

  • 计算属性

    import store from './store'
    const Counter = {
      template: `<div>{{ count }}</div>`,
      computed: {
        count () {
          return store.state.count
        }
      }
    }
    

    然而,组件可能需要频繁导入store

  • store选项

    //父组件
    import store from './store'
    import Counter from './Counter'
    new Vue({
      el: '#app',
      store,	//将store从根组件“注入”到每一个子组件中
      render:c=>c(Counter),
    })
    
    //子组件Counter.vue能通过 `this.$store` 访问:
    <template>
    <div>{{count}}</div>
    </template>
    
    <script>
    export default {
        computed: {
        count () {
          return this.$store.state.count
        }
      }
    }
    </script>
    

辅助函数mapState

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

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

export default new Vuex.Store({
    state:{
        count:4
    }
})
//Counter.vue

<template>
    <div>{{sumCount}}</div>
</template>

<script>
import store from './store'
import {mapState} from 'vuex' //引入辅助函数
export default {
  data(){
    return{
      localCount:7
    }
  },
  computed:mapState({
        count:state=>state.count, //返回count,等同于count () {return this.$store.state.count}
        countAlias:'count', //count的别名是countAlias,这时候countAlias等同于count(注意单引号)

        sumCount(state){ //处理多状态:store的数据加上本地的数据
          return state.count+this.localCount
        }
  })

}
</script>

当映射的计算属性的名称与 state 的子节点名称相同时,可以给 mapState 传一个字符串数组:

computed: mapState([
  'count'  //等同于count:state=>state.count,
])

2. Getter

前言

假设我们需要处理store中state,例如对列表进行过滤并计数

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

然而如果很多组件都需要用到这个计算属性,那么我们只能复制这个函数,或者频繁导入它。

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

应用

1. 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)
        }
      }
    })
    
  • getter接受第二个参数getter

    getters: {
      doneTodos: state => {
          return state.todos.filter(todo => todo.done)
      }
      doneTodosCount: (state, getters) => {
        return getters.doneTodos.length //通过getter访问到doneTodos属性
      }
    }
    

2. 通过属性访问

//访问
store.getters.doneTodos 
store.getters.doneTodosCount 
//组件中使用
computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

3. 通过方法访问

让 getter 返回一个函数,来实现给 getter 传参。这对在 store 里的数组进行查询时非常有用

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
//访问
store.getters.getTodoById(2) //返回元素id与传过去的id相等的元素

辅助函数mapGetters

import { mapGetters } from 'vuex'

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

3. Mutation

简单示例

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。 mutation 类似于事件:每个 mutation 都有一个 事件类型 (type) 和 一个 回调函数 (handler)。它接受 state 作为第一个参数:

const store = new Vuex.Store({
  state: {
    count: 1
  },
    
  mutations: {
    //事件类型:increment;回调函数为后面部分;
    increment (state) {
      state.count++
    }
  }
})

想要调用该事件类型的回调,需要使用store.commit(),并传入字符串的事件类型

store.commit('increment')

提交载荷(Payload)

可以向 store.commit 传入额外的参数,这些额外参数就是 mutation 的 载荷(payload)

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

在大多数情况下,载荷应该是一个对象,这样更易读:

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

提交方式

1. 没有载荷

store.commit('事件类型')

2. 有载荷:

//一般写法
store.commit('increment', 10)
//载荷为一个对象时
store.commit('事件类型', {
  amount: 10
})
//载荷为一个对象时-对象风格写法
store.commit({
  type: '事件类型',
  amount: 10
})

3. 组件中提交

this.$store.commit('事件类型')

mapMutations辅助函数

methods:{
    //...
    ...mapMutations([
      'increment',  //相当于this.$store.commit('increment')
      'incrementBy', //相当于`this.$store.commit('incrementBy', n)`
      'incrementBy2' //相当于this.$store.commit('incrementBy2',{amount})
    ]),
    ...mapMutations({
      add:'increment' //此时this.add相当于$store.commit('increment')
    })
  }

注意:Mutation 必须是同步函数

使用常量代替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
    }
  }
})

4. Action

前言

Action 类似于 mutation,不同在于:

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

简单示例

//一个往数组添加随机数的实例:
const store = new Vuex.Store({
  state: {
    msg: []
  },
  mutations: {
    addMessage(state,msg){
      state.msg.push(msg)
    }
  },
  
//一个异步操作:
actions: {
     getMessages(context){
       fetch('https://www.easy-mock.com/mock/5f4a32907e1a7f3146e313e7/api/getnum')
         .then(res=>res.json())
         .then(data=>{
           context.commit('addMessage',data.data.number)
         })       
     }
 }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以在这个对象中获取 state 和 getters或提交一个 mutation。

ES6中可以通过参数解构简化代码

actions: {
     getMessages({commit}){
       //...
           commit('addMessage',data.data.number)
       //...      
     }
 }

分发Action

1. 普通分发

Action 通过 store.dispatch 方法触发,

store.dispatch('getMessages')

2. 载荷分发

// 普通
store.dispatch('事件类型', {
  amount: 1
})

// 对象形式
store.dispatch({
  type: '事件类型',
  amount: 1
})

3. 组件中分发

this.$store.dispatch('事件类型')

mapMutations辅助函数

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'getMessages', // 相当于`this.$store.dispatch('getMessages')`
    ]),
    ...mapActions({
      addmsg: 'getMessages' //this.addmsg等同于`this.$store.dispatch('getMessages')`
    })
  }
}

组合Action

如何知道 action 什么时候结束呢?如何才能组合多个 action,以处理更加复杂的异步流程呢?首先要知道:

  • store.dispatch 可以处理被触发的 action 的处理函数返回的 Promise
  • store.dispatch 仍旧返回 Promise:
actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

现在可以指定回调:

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

也可以 另一个action指定回调:

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

最后利用 async / await,我们可以如下组合 action:

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

5. Module

前言

当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块:

const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

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

模块的局部状态

const moduleA = {
  state: () => ({
    count: 0
  }),
    
  //1. 对于mutation,接收的第一个参数是模块的局部状态对象。
  mutations: {
    increment (state) {
      state.count++
    }
  },
  //2. 对于getter,第一个参数暴露局部状态;第三个参数`rootState`暴露根节点状态
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
    
  //3. 对于action,局部状态通过 context.state 暴露出来,根节点通过 context.rootState:
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

命名空间

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

如果希望你的模块具有更高的封装度和复用性,你可以在模块对象中加入 namespaced: true 使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名

const store = new Vuex.Store({
  modules: {
    account: {
      namespaced: true,
      state: () => ({ ... }), 
      getters: {isAdmin () { ... }},// 通过 store.getters['account/isAdmin']访问
      actions: {login () { ... }},// 通过 store.dispatch('account/login')分发
      mutations: {login () { ... }},//通过store.commit('account/login')提交

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

        //模块2-mposts-进一步嵌套命名空间
        posts: {
          namespaced: true,
          state: () => ({ ... }),
          getters: {popular () { ... }}//store.getters['account/posts/popular']
        }
      }
    }
  }
})

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

rootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatchcommit 即可。

modules: {
  foo: {
    namespaced: true,
    getters: {
      someOtherGetter: state => { ... }
      //1. 对于getter,使用第四个参数 `rootGetters`访问根getter
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 局部的
        rootGetters.someOtherGetter // -> 全局的
      },
    },

    actions: {
      someOtherAction (ctx, payload) { ... }
	  //2. 对于actions,接受 root 属性来访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        dispatch('someOtherAction') // -> 局部的分发
        dispatch('someOtherAction', null, { root: true }) // -> 全局的分发

        commit('someMutation') // -> 局部的提交
        commit('someMutation', null, { root: true }) // -> 全局的提交
      },
    }
  }
}

在带命名空间的模块注册全局 action

在action中添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

{
  //...
  modules: {
    foo: {
      namespaced: true,
      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 定义someAction action
        }
      }
    }
  }
}

带命名空间的绑定函数

当使用 mapState, mapGetters, mapActionsmapMutations 这些函数来绑定带命名空间的模块时:

computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  })
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文,简化后:

computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  })
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

简化的另一个方法:使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')//基于某个命名

export default {
  computed: {
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

实例源码

posted @ 2020-09-01 14:21  sanhuamao  阅读(704)  评论(0编辑  收藏  举报