如果你看不懂vuex的官方教程,那就对了,因为他们写的不够平民化,而且用了很多术语来迷惑对手,来看看我翻译的平民版教程吧。
问:VUEX有什么用?
答:管理全局变量和相关的全局方法。当你需要子子通信的时候就知道了,如果一层一层的传值一定会把你烦死,所以你需要一个公用的全局对象去管理一些全局的变量和方法。
vuex就是eventbus(vue2.0之后改名叫store模式了)的升级版。jquery时代和backbone时代可以使用观察者模式实现,也有人使用nodejs里面提取的EventEmitter.js来实现“状态管理”,不过那时候还没有“状态管理”这么高大尚的词,我们都叫事件管理或者事件模型。
store 模式
这是一个精简版的vuex
// 定义全局对象 store var store = { debug: true, state: { message: 'Hello!' }, setMessageAction (newValue) { if(this.debug) console.log(’setMess',newValue) this.state.message = newValue }, clearMessageAction () { if(this.debug) console.log(’clearMess') this.state.message = '' } } // 组件里面声明好那些属性是私有的哪些是公共的 var vmA = new Vue({ data: { privateState: {}, sharedState: store.state } }) var vmB = new Vue({ data: { privateState: {}, sharedState: store.state } })
store内所有state值变化的操作方法都要放到store对象自身的方法里面去实现。然后再用一个log来记录每次操作。这样控制台里面就能看到所有与state变化的相关操作记录了。
基于上面的简单store模式,继续延伸约定。你不应该在各自的子组件里面直接修改store内的state里面的值。应该定义一个全局的公共对象来执行action来分发事件通知store去改变才行,这才是flux架构。
flux架构的好处是不仅能够管理store中state改变的记录。还能记录变更、保存状态快照、历史回滚/时间旅行。如果不理解这些概念就往下面看看vuex的思想吧
1. State
单一状态树 —— 只用1个对象来保存所有数据源,这样每个应用就只可以有1个store对象。相当于以前的全局对象。
1.1 在vue组件中获得vuex状态
其实跟读取data中的数据一样,使用computed方法,把store里面的属性返回出来
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } }
1.2 mapState 辅助函数
当需要读取store对象内多个属性的时候,如果写多个computed方法是不是太麻烦,而且代码冗余,mapState就是为了解决这个问题
computed: mapState({ // 箭头函数可使代码更简练 count: state => state.count, // 传字符串参数 'count' 等同于 `state => state.count` countAlias: 'count', // 为了能够使用 `this` 获取局部状态,必须使用常规函数 countPlusLocalState (state) { return state.count + this.localCount } }) // 计算属性的名称与 state 的子节点名称相同时 computed: mapState([ // 映射 this.count 为 store.state.count 'count' ]) // 对象展开运算符 computed: { localComputed () { /* ... */ }, // 使用对象展开运算符将此对象混入到外部对象中 ...mapState({ // ... }) }
2. Getter
store中的state只是保存固定的单一数据就相当于vue组件里的data属性一样。那么getter就相当于vue组件里的computed。
下面代码中的state换成data, getters换成computed是不是就理解了
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) } } })
2.1 调用方式
// 通过属性访问 store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }] // 通过方法访问 // 通过让 getter 返回一个函数,来实现给 getter 传参 getters: { // ... getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } } store.getters.getTodoById(2)
2.2 mapGetters 辅助函数
跟mapState类似
computed: { // 使用对象展开运算符将 getter 混入 computed 对象中 ...mapGetters([ 'doneTodosCount', 'anotherGetter', // ... ]) }
3. Mutation
3.1 mutation更改store中state属性值的唯一方法,把mutation换成setStates是不是就好理解了
const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 变更状态 state.count++ } } })
但是mutation毕竟不是setState,根据约定,你不可以直接调用mutation内声明的方法,因为mutation只负责注册事件。那么如何触发它定义的事件呢?这样:
store.commit('increment’) //把commit换成trigger是不是更好理解
3.2 提交载荷(Payload)
触发事件方法的参数你就说参数,非要起个难理解的名词来显示自己很高大上?
mutations: { increment (state, payload) { state.count += payload.amount } } // 注意payload参数要用对象的形式,因为人家这么约定的 store.commit('increment', { amount: 10 }) // 另外一种写法 store.commit({ type: 'increment', amount: 10 })
3.3 Mutation 必须是同步函数,里面不要出现异步操作,因为异步操作你就不知道每个方法执行的顺序了。
3.4 在子组件中提交Mutation
你可以直接在方法里面使用this.$store.commit(‘xxx’)或者使用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')` }) } }
4. Action
上面说到Mutation必须要使用同步函数,那么Action就是来解决你想使用异步函数的这个事情。先看一下Action 怎么写:
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { // 注意context这个参数有点不一样 context.commit('increment') } } })
乍一看context是不是就等于store,最后介绍Moudles的时候你就知道区别了。实践中一般都是使用ES2015的参数解构方式简化代码:
actions: { increment ({ commit }) { commit('increment') } }
4.1 分发Action
store.dispatch('increment')
乍一看是不是跟mutations没什么区别,实际上真就没什么区别,atciont其实就是专门为了异步操作来设计的。如果你有异步的方法就写在action里面,如果是同步的就写在mutations里面,就这么简单,别想复杂了。
actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }
官网那个购物车示例如果看不懂,看一下这个平民版:
actions: { checkout ({ commit, state }, products) { const id = state.id const savedCartItems = state.cart.items $ajax.postProduct({ data:{ id, products } }).then(res =>{ commit(types.CHECKOUT_REQUEST) }).catch(res =>{ commit(types.CHECKOUT_FAILURE, savedCartItems) }) } }
5. Module
我们知道一个应用只允许使用一个store,这样做有个问题就是,当业务越来越复杂的时候,这个对象里面就会有N多的属性和方法,所以我们要把store拆成一个个的小份,然后根据业务去分类,以便维护。
Module其实就是为了让你的代码看起来更整洁的,如果业务没那么复杂不用也可以。
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 的状态
5.1 模块的局部状态
注意参数mutation和getter方法里面的第一个参数是模块的局部状体对象
const moduleA = { state: { count: 0 }, mutations: { increment (state) { // 这里的 `state` 对象是模块的局部状态 state.count++ } }, getters: { doubleCount (state) { return state.count * 2 } } }
根节点状态用context.rootState表示:
const moduleA = { // … getters: { sumWithRootCount (state, getters, rootState) { return state.count + rootState.count } }, actions: { incrementIfOddOnRootSum ({ state, commit, rootState }) { if ((state.count + rootState.count) % 2 === 1) { commit('increment') } } } }