vuex 官方教程平民翻译版 – 核心概念

如果你看不懂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')
      }
    }
  }
}