wuhuihao
Frontend Developer
Hangzhou,Wenzhou

什么是Vuex[翻译]

什么是Vuex

Vuex是一个Vue.js程序的状态管理模式+类库。它作为一个中央仓库服务于程序中的所有组件,遵循一定的规则保证状态的变化发生在可预测的方法中。他也集成了Vue官方的devtools extension能提供一些高级的特性,类似零配置的time-travel调试和状态快照的导入导出。

什么是”State Management Pattern”?

让我从一个简单的vue程序开始

new Vue({
  // state
  data () {
    return {
      count: 0
    }
  },
  // view
  template: `
    <div>8</div>
  `,
  // actions
  methods: {
    increment () {
      this.count++
    }
  }
})

它是一个包含如下部分的自给自足的app:

  1. state:驱动你的app的真实数据来源。
  2. view:state映射的声明
  3. actions:可以根据从view中获取的用户输入从而改变state值的方法

这是非常简单的一个例子体现了”one-way data flow”的概念

Alt text

当我们拥有多个共享同一state的组件之后,这种关系很快就被打破了。

  1. 多个view依赖于同一块state
  2. 不同的view里面的方法可能需要改变同一块state

对于问题1,在深层嵌套的组件中传递属性将会是单调乏味的,而且不能在相邻的组件之间起作用。对于问题2,会发现我们会常常借助于下面的一些方法,获取指向父级/子级实例的引用或者在事件中尝试改变和同步state的多个副本。这些方法都是脆弱的,而且容易导致很多不可维护的代码。

所以我们为什么不把共享的state从组件中提取出来,放在一个公共的单例中进行管理。如果这样,我们的组件树将会变成一个巨大的“view”, 我们组件不管在树中的哪里都能访问state或者触发事件.

除此之外,通过定义和分离状态管理中涉及的概念并执行一定的规则,使我们代码更具结构性和可维护性。

这是Vuex背后的基本概念,灵感来自于Flux, Redux 和 The Elm Architecture,与其他的模式不同,Vuex也是一个为Vue量身定做的类库,利用其颗粒反应系统进行有效的更新。

Alt text

###Get Started

每一个Vuex程序的中心是Store。一个“store”从根本来说就是一个持有你程序state的容器。与单纯的公共对象比,Vuex Stroe有两点不同。

  1. Vuex store是具有反应性的,当组件从它当中检索数据的时候,他会对此反应并高效的更新自己如果有state发生改变的话。
  2. 你不能直接改变Store中state的值,明确的调用committing mutations是改变Store中state的唯一方法。这个保证了每次state的改变都留下了一条可追踪的记录,使得一个工具能帮我们更好的了解我们的程序。

###The Simplest Store

安装完Vuex之后,创建一个非常简单的store,只提供一个初始的状态对象和一些简单的mutation。

//如果使用module系统要先确保调用了Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

先在我们可以访问定义为store.state的状态对象了,还可以通过调用store.commit方法触发状态的改变。

store.commit('increment')

console.log(store.state.count) // -> 1

再次说明,为了明确的追踪状态的变化,我们使用committing a mutation代替了直接去改变store.state.count的方法。这个简单的约定使你的意图更加明确,在你阅读代码的时候能更好的了解状态改变的原因。除此之外,还能使我们的工具记录所有状态改变的日志,记录状态的快照或者更好的实现代码时间穿越的调试。

要在组件中使用Store state,只需影响一个计算属性的返回状态,因为store state 是具有反应性的。触发事件只需简单的在组件的方法中调用committing mutations。

##state

###Single State Tree

Vuex使用一个单例状态树,这个单独的对象包含了所有应用级别的状态并且作为单独的真实数据来源。这意味在通常情况下你将在你的每个应用中只拥有一个store。使用一个单例状态树,能使定位某一块特殊的state的操作变得更加简单直接,而且允许我们能方便得得到一个当前应用状态的快照用于调试。

单例状态树并不与我们的模块化相冲突,在后面的章节中,我们将探讨如何去分离你的状态并变成子模块。

###Getting Vuex State into Vue Components

所以我们如何在vue的组件中展示Store中的state呢?因为Vuex store是具有反应性的,要从检索它里面检索state,最简单的方法就是在一个computed property返回一些store state。

// let's create a Counter component
const Counter = {
  template: `<div>8</div>`,
  computed: {
    count () {
      return store.state.count
    }
  }
}

无论何时store.state.count发生改变,都将会导致computed property的重新评估,并触发关联DOM的更新。

然而这个模式导致组件将会依赖于一个公共store单例。当使用module系统时,需要在每个使用store state的组件中引入store,而且需要在测试组件的时候进行模拟。

Vuex提供一个机制使得跟组件能将store注入到每个子组件中,通过一个store选项配置(通过Vue.use(Vuex)启用)。

const app = new Vue({
  el: '#app',
  // provide the store using the "store" option.
  // this will inject the store instance to all child components.
  store,
  components: { Counter },
  template: `
    <div class="app">
      <counter></counter>
    </div>
  `
})

通过在根实例中配置store选项,store将会被注入到每一个子组件中,子组件就可以通过this.$store去使用,让我们更新我们的Counter去实现。

const Counter = {
  template: `<div>8</div>`,
  computed: {
    count () {
      return this.$store.state.count
    }
  }
}

###The mapState Helper

当一个组件需要利用多个store state 属性或getter时,在每个computed properties中声明它们将会是繁琐和重复的,我们可以使用mapState helper去应对这种情况,它生成了computed getter方法给我们,帮助我们保存一些按键信息。

// in standalone builds helpers are exposed as Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // arrow functions can make the code very succinct!
    count: state => state.count,

    // passing the string value 'count' is same as `state => state.count`
    countAlias: 'count',

    // to access local state with `this`, a normal function must be used
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })

当映射的计算属性名称跟store状态树节点的名称相同时,我们可以只传递一个字符串的数组。

computed: mapState([
  // map this.count to store.state.count
  'count'
])

###Object Spread Operator

mapState 返回的是一个整个Object,我们要如何与其他local计算属性配合使用。通常我们会使用utility去合并多个对象到一个对象,然后传给computed。然而通过object spread operator,我们可以大大的简化语法。

computed: {
  localComputed () { /* ... */ },
  // mix this into the outer object with the object spread operator
  ...mapState({
    // ...
  })
}

###Components Can Still Have Local State

使用Vuex并不意味着你需要把所有的状态推送到Vuex,当然把更多的状态放到Vuex中管理,能让状态的变化变得更加明显和可调试。有时候这可能让代码变得更加啰嗦和迂回。如果一部分状态只属于一个独立的组件,那它就可以脱离只作为局部状态,你需要根据你的应用做出最合适的决定。

###Getters

有时候我们需要基于store state计算得出派生状态值,例如过滤一整个列表并计算它们:

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

如果多个组件都需要如此地使用,我们需要重复相同的函数,或者把它提取到一个共享的Helper中并在多个地方引用,两种方法都不理想。

Vuex允许我们在Stroe中定义 “getters” 。getters将会接收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)
    }
  }
})

定义好的getters将会暴露在store.getters中:

store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]

Getters将其他定义好的getters最为第二个参数传入

getters: {
  // ...
  doneTodosCount: (state, getters) => {
    return getters.doneTodos.length
  }
}
store.getters.doneTodosCount // -> 1

我们可以轻松的在组件内部适应

computed: {
  doneTodosCount () {
    return this.$store.getters.doneTodosCount
  }
}

###The mapGetters Helper

mapGetters简单的映射store getters到local 计算属性:

import { mapGetters } from 'vuex'

export default {
  // ...
  computed: {
    // mix the getters into computed with object spread operator
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
}

如果你想用一个不一样的名字去映射getter,可以使用一个对象:


...mapGetters({
  // map this.doneCount to store.getters.doneTodosCount
  doneCount: 'doneTodosCount'
})

提交一个mutation是改变一个Vuex store中状态的唯一方法,Vuex mutations与events十分相似,每一个mutation都有一个字符串名称和一个函数句柄。在句柄函数中我们执行状态的修改,并将状态作为第一个参数接收:

 const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // mutate state
      state.count++
    }
  }
})

你不能直接调用mutation的函数句柄,这里的操作会跟事件注册更相似:“当一个type为increment的mutation被触发,会调用它的句柄”。 要调用一个mutation的句柄,你需要调用store.commit并传入它的type

###Commit with Payload

调用store.commit的时候我们可以传入一个额外的参数,我们称之为mutation的payload。

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

在大多数情况下,传入的payload将会是一个对象,这将能包含更多的字段,并使得突变的记录变得更具可读性。

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

###Object-Style Commit

一个替代的方法去提交突变是传入一个带有type属性的对象

store.commit({
  type: 'increment',
  amount: 10
})

我们使用对象风格的提交,整个对象将会被当做payload传递给突变的句柄,随意句柄依旧是一样的。

  mutations: {
  increment (state, payload) {
    state.count += payload.amount
  }
}

###Mutations Follow Vue’s Reactivity Rules

从一个Vuex store’s state被Vue变得具有反应性后,当我们改变一个状态,正在观察这个状态的Vue components将会自动的更新。这也意味着,Vuex mutations 也服从于相同的反应性告诫在一个单纯的vue环境中。

1.最好在你需要的所有字段之前初始化你的stroe的初始状态。

2.当你增加一个新的属性到一个对象的时候,你需要:

.使用Vue.set(obj, ‘newProp’, 123), 或者使用一个新的对象替换

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

###Using Constants for Mutation Types

使用固定不变的Mutation Types 在各种Flux的实现中是十分常见的。这样做可以从像linters这样的工具中获取优势,并且把所有的常量放到一个文件中,能让你的合作同事更好地了解到有哪些mutations可以在整个应用中使用。

 // 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: {
    // we can use the ES2015 computed property name feature
    // to use a constant as the function name
    [SOME_MUTATION] (state) {
      // mutate state
    }
  }
})

使用常量对于多人合作的大型项目来说是十分有益的,但这都是可配置取决于你是否喜欢。

###Mutations Must Be Synchronous

你需要记住一个非常重要的规则,mutation句柄函数必须是同步的。我们可以考虑如下的例子:

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

想象一下我们在调试app,并且正在查看mutation日志信息,每一个突变被记录,开发者工具需要记录这个状态之前和之后的快照。但是,这个例子中异步回调的突变使得这个操作变得不可能。当mutation被提交的时候,回调还并未被调用,而且开发者并不能知道这个回调到底是否会被调用,将无法追踪在回调中发生的状态变化。

###Committing Mutations in Components

你可以使用this.$store.commit(‘xxx’)在组件中提交mutation,或者我们可以使用mapMutations Helper ,他将映射组件方法到store.commit调用

import { mapMutations } from 'vuex'

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment' // map this.increment() to this.$store.commit('increment')
    ]),
    ...mapMutations({
      add: 'increment' // map this.add() to this.$store.commit('increment')
    })
  }
}

###On to Actions

在状态mutation中结合异步操作,将会使你的程序变得难以推论。举个例子,当你在改变状态的方法中同时调用两个异步的方法,你不知道回调什么时候被调用,也不知道哪个回调会被先调用。这就是为什么我们会分开为两个概念,在Vuex中,mutations are synchronous transactions。

store.commit('increment')
// any state change that the "increment" mutation may cause
// should be done at this moment.

Actions跟mutations 相似,有如下的两点不同:

  1. 不再直接修改state,而是提交mutations
  2. Actions可以包含任意的一步操作

让我们注册一个简单的action

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 的句柄接收一个上下文对象作为参数,这个对象暴露了与Stroe相同的对象和属性,所以你可调用context.commit去提交一个mutation,或者通过context.state 和 context.getters获取state和getters。

在实际编写时,我们会通过使用ES2015的解构语法简化我们的代码。

actions: {
  increment ({ commit }) {
    commit('increment')
  }
}

###Dispatching Actions

Action 被 store.dispatch方法触发

我们可以在Action实现异步操作:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

Actions同样支持payload模式和对象风格的dispatch

// dispatch with a payload
store.dispatch('incrementAsync', {
  amount: 10
})

// dispatch with an object
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

一个更现实的例子是查看购物车,结合了调用异步API和committing multiple mutations:

actions: {
  checkout ({ commit, state }, products) {
    // save the items currently in the cart
    const savedCartItems = [...state.cart.added]
    // send out checkout request, and optimistically
    // clear the cart
    commit(types.CHECKOUT_REQUEST)
    // the shop API accepts a success callback and a failure callback
    shop.buyProducts(
      products,
      // handle success
      () => commit(types.CHECKOUT_SUCCESS),
      // handle failure
      () => commit(types.CHECKOUT_FAILURE, savedCartItems)
    )
  }
}

注意我们实现了一系列一步操作,并通过commit方法记录了副作用 (state mutations) ;

###Dispatching Actions in Components

你可以通过调用this.$store.dispatch(‘xxx’)分发Action在组件中,或者使用mapActions helper映射组件的方法到store.dispatch。

import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment' // map this.increment() to this.$store.dispatch('increment')
    ]),
    ...mapActions({
      add: 'increment' // map this.add() to this.$store.dispatch('increment')
    })
  }
}

Action通常是异步的,所以我们要怎么才能知道一个Action已经结束了?还有更重要的是,我们要如何组合多个action一起去处理更复杂的异步流。

你需要知道store.dispatch能处理由一个被触发的action返回的promise,而且它自身也能返回一个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,我们还可以如此组合actions

actions: {
  async actionA ({ commit }) {
    commit('gotData', await getData())
  },
  async actionB ({ dispatch, commit }) {
    await dispatch('actionA') // wait for actionA to finish
    commit('gotOtherData', await getOtherData())
  }
}

store.dispatch可能会触发多个action不同的moudle中,再这样的情况下返回的值将会是一个promise,它将解决什么时候所有触发的句柄已经全部执行完毕的问题。

###Modules

由于使用一个仅有的状体树,程序所包含的所有状态全都在一个对象中,随着程序的壮大,store将会变得十分臃肿。

为了解决这个问题,Vuex允许我们将store分解到多个Moudle中。每一个Moudle拥有它的state, mutations, actions, getters, 和 nested modules。

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's state
store.state.b // -> moduleB's state

###Module Local State

在模块内的mutations 和 getters,接收到的参数将是模块自身的局部state

const moduleA = {
  state: { count: 0 },
  mutations: {
    increment: (state) {
      // state is the local module state
      state.count++
    }
  },

  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

相似的,在模块内部的actions,context.state将会暴露局部state,根状态将通过context.rootState暴露。

const moduleA = {
  // ...
  actions: {
    incrementIfOdd ({ state, commit }) {
      if (state.count % 2 === 1) {
        commit('increment')
      }
    }
  }
}

在模块内的getters中,根状态将作为第三个参数传入。

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