vuex官方文档自学笔记

110 阅读7分钟

介绍

vuex是一个专为vue.js开发的状态管理模式+库

什么是“状态管理模式”?

让我们从一个简单的 Vue 计数应用开始:

const Counter = {
  // 状态
  data () {
    return {
      count: 0
    }
  },
  // 视图
  template: `
    <div>{{ count }}</div>
  `,
  // 操作
  methods: {
    increment () {
      this.count++
    }
  }
}

createApp(Counter).mount('#app')

这个状态自管理应用包含以下几个部分:

  • 状态,驱动应用的数据源;
  • 视图,以声明方式将状态映射到视图;
  • 操作,响应在视图上的用户输入导致的状态变化。

以下是一个表示“单向数据流”理念的简单示意:

安装

npm install vuex@next --save

最简单的store

import{ createStore } from 'vuex'  

//创建一个新的store实例
const store = createStore({  
    state(){  
        return{  
            count:0  
           }  
           },  
    mutations:{
        increment(state){
            state.count++
            }
            }
           })
const app = createApp({/*跟组件*/})
//将store实例作为插件安装
app.use(store)

通过store.state获取状态对象
通过store.commit触发状态变更

store.commit('increment')
console.log(store.state.count)

通过this.$store访问store实例

methods:{
    increment(){
        this.$store.commit('increment')
        console.log(this.$store.state.count)}
        }

通过提交(commit)mutation的方式

核心概念

State

单一状态树

vuex使用单一状态树,也就意味着,每个应用仅仅包含一个store实例

vue组件获得vuex状态

通过在计算属性中返回某个状态

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

这样每次store.state.count发生变化的时候,重新算的计算属性computed,然后触发更新dom
这样的模式导致组件依赖全局状态单例巴拉巴拉。。需要在每个需要state的组件中频繁的导入

所以(更方便的)vuex通过vue的插件系统将store实例通过根组件“注入”到所有子组件里 子组件能通过this.$store访问到

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

mapState辅助函数

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

// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount
    }
  })
}

组件仍然保有局部状态

使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。

Getter

有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:

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

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。

Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。

Getter接受state作为其第一个参数

const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos (state) {
      return state.todos.filter(todo => todo.done)
    }
  }
})

通过属性访问

Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:

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

Getter 也可以接受其他 getter 作为第二个参数:

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

我们可以很容易地在任何组件中使用它:

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

注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。

通过方法访问

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

getters: {
  // ...
  getTodoById: (state) => (id) => {
    return state.todos.find(todo => todo.id === id)
  }
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }

注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。

mapGetters辅助函数

mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:

import { mapGetters } from 'vuex'

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

如果你想将一个 getter 属性另取一个名字,使用对象形式:

...mapGetters({
  //`this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

Mutation

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation
Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type) 和一个回调函数(handler)..increment(state) 。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:

const store = createStore({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

不能直接调用mutation处理函数,而是事件注册,当触发一个类型为increment的mutation时,调用此函数,要唤醒(调用)函数,需要以相应的type调用store.commit方法

store.commit('increment)

提交载荷

即提交store.commit的过程中传入额外的参数,即mutation的载荷

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

在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:

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

对象风格的提交方式

提交mutation的另一种方式是直接使用包含type属性的对象

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

当使用对象风格的提交方式,整个对象都作为载荷传给mutation函数,因此处理函数保持不变

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

使用常量替代 Mutation 事件类型

即,用常量代替上文中的increment 即函数名

// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = createStore({
  state: { ... },
  mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能
    // 来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
      // 修改 state
    }
  }
})

Mutation必须是同步函数

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

现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。

组件中提交Mutation

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

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

mutation只能为同步事务,才好调试,所以引出Action

在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?这就是为什么我们要区分这两个概念。在 Vuex 中,mutation 都是同步事务

store.commit('increment')
// 任何由 "increment" 导致的状态变更都应该在此刻完成。

Action

Action 类似于 mutation,不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。(即:Action包含的回调increment函数是执行提交..commit('increment'),而mutation是直接改变状态...count++
  • Action 可以包含任意异步操作。
const store = createStore({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

分发Action

Action通过store.dispatch方法分发action,或者使用mapActions辅助函数将组件的methods映射为store.dispath调用(需要先在根节点注入store):

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

Action通常是异步的,为了知道action什么时候结束和如何组合多个action,处理更加复杂的异步流程 ,我们可以使用store.dispach处理被触发的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 函数是一个返回 Promise 对象的函数,它内部使用了 await 表达式。await 表达式可以暂停 async 函数的执行,等待 Promise 对象的解析结果,然后继续执行 async 函数。

如果我们利用async/await,我们可以如下组合action

// 假设 getData() 和 getOtherData() 返回的是 Promise

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

Module

使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变的非常复杂时,store对象就可能变的很臃肿,为了解决上面的问题,vuex允许我们将store分割成模块,每个模块拥有自己的state、mutation、action、getter、和嵌套子模块。意思就是定义一个模块,里面包含state,mutations,...等等

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

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

const store = createStore({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

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

模块的局部状态

对于模块内部的mutation和getter,接受的一个参数是模块的局部状态对象

const moduleA = {
    state:() => ({
      count:0
      }),
      //改变状态的唯一方法 函数接受参数的都是模块里面的那个状态
    mutations:{
       increment (state) {
           state.count++
           }
           },
     //类似于状态的计算属性
    getters: {
       doubleCount(state){
           return state.count * 2
           }
          }
         }

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


const moduleA = {
//...
actions:{
    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
        }
       }
      }

命名空间

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


const store = createStore({
  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']
          }
        }
      }
    }
  }
})

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

在带命名空间的模块内访问全局内容(Global Assets)

如果要使用全局state和getter,rootState和rootGetters会作为第三和第四参数传入getter,也会通过context对象的属性传入action。

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

modules:{
  foo:{
      namespaced: true,
      getters: {
      // 在这个模块的getter中,'getters'被局部化了
      // 可以用getter的第四个参数来调用`rootGetters`
      someGetter(state,getters,rootState,rootGetters){
          getters.someOtherGetter // -> 'foo/someOther’
          rootGetters.someOtherGetter // ->'someOtherGetter'
          rootGetters['bar/someOtherGetter']// ->'bar/someOtherGetter'
          },
          someGetter: state = {....}
          },
          
 actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'
        rootGetters['bar/someGetter'] // -> 'bar/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')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}

而且可以通过使用createNamespacedHelpers创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

import{ createNamespacedHelpers } from 'vuex'
const{ mapState,mapAction } = createNamespacedHelpers('some/nested/module')
export default{
    computed:{
        //在'some/nested/module'中查找
        ...mapState({
           a: state => state.a,
           b: state => state.b
           })
           },
   
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

模块动态注册

在store创建之后,可以使用store.registerModule方法注册模块

import { createStore } from 'vuex'
const store = createStore({/* 选项*/})
//注册模块`myModule`
store.registerModule('myModule',{
//...
})
//注册嵌套模块`nested/myModule`
store.registerModule(['nested','myModule'],{
//....
})

之后就可以通过store.state.myModule和store.state.nested.myModule访问模块的状态。 模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync 插件就是通过动态注册模块将 Vue Router 和 Vuex 结合在一起,实现应用的路由状态管理。

你也可以使用 store.unregisterModule(moduleName) 来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。

注意,你可以通过 store.hasModule(moduleName) 方法检查该模块是否已经被注册到 store。需要记住的是,嵌套模块应该以数组形式传递给 registerModule 和 hasModule,而不是以路径字符串的形式传递给 module。

保留state

在注册一个新module时,要保留过去的state,例如从服务端渲染的应用保留state,可以通过preserveState选项将其归档:store.registerModule('a',module,{preserveState:true})。 当设置preserveState: true时,该模块会被注册,action、mutation、和getter会被添加到store中,但state不会,这里假设store的state已经包含了这个module的state并且你不希望将其覆写。

模块重用

有时我们可能需要创建一个模块的多个实例,例如:

如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。

实际上这和 Vue 组件内的 data 是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):

const MyReusableModule = {
  state: () => ({
    foo: 'bar'
  }),
  // mutation、action 和 getter 等等...
}

项目结构

1:应用层级的状态应该集中到单个store对象中 2:提交mutation是更改状态的唯一方法,并且这个过程是同步的 3:异步逻辑都应该封装到 action里面

如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。

对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:

├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块

组合式api

通过调用useStore函数,来在setup钩子函数中那个访问store。这与在组件中使用选项式api访问this.$store是等效的。

import {useStore} from 'vuex'
export default{
  setup(){
      const store = useStre()
      }
    }

访问State和Getter

为了访问state和getter,需要创建computed引用以保留响应性,这与在选项式api中创建计算属性等效

import { computed } from 'vue'
import { useStore } from 'vuex'
export default{
    setup(){
        const store = useStore()
        return{
            //在computed函数中访问state
            count:computed(() => store.state.count),
            //在computed函数中访问getter
            double:computed(() => store.getters.double)
            }
           }
          }

访问Mutation和Action

要使用 mutation 和 action 时,只需要在 setup 钩子函数中调用 commit 和 dispatch 函数。

import { useStore } from 'vuex'

export default {
  setup () {
    const store = useStore()

    return {
      // 使用 mutation
      increment: () => store.commit('increment'),

      // 使用 action
      asyncIncrement: () => store.dispatch('asyncIncrement')
    }
  }
}

插件

Vuex 的 store 接受 plugins 选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:

const myPlugin = (store) => {
  // 当 store 初始化后调用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之后调用
    // mutation 的格式为 { type, payload }
  })
}

然后像这样使用:

const store = createStore({
  // ...
  plugins: [myPlugin]
})

在插件内提交Mutation

在插件中也是类似于组件,只能通过提交mutation来触发变化,通过提交mutation,插件可以同步数据源到store,例如同步websocket数据源到store

export default function createWebSocketPlugin (socket) {
  return (store) => {
    socket.on('data', data => {
      store.commit('receiveData', data)
    })
    store.subscribe(mutation => {
      if (mutation.type === 'UPDATE_DATA') {
        socket.emit('update', mutation.payload)
      }
    })
  }
}
const plugin = createWebSocketPlugin(socket)

const store = createStore({
  state,
  mutations,
  plugins: [plugin]
})

生成state快照

有时候插件需要获得状态的“快照”,比较改变的前后状态。想要实现这项功能,你需要对状态对象进行深拷贝:

const myPluginWithSnapshot = (store) => {
  let prevState = _.cloneDeep(store.state)
  store.subscribe((mutation, state) => {
    let nextState = _.cloneDeep(state)

    // 比较 prevState 和 nextState...

    // 保存状态,用于下一次 mutation
    prevState = nextState
  })
}

生成状态快照的插件应该只在开发阶段使用,使用 webpack 或 Browserify,让构建工具帮我们处理:

const store = createStore({
  // ...
  plugins: process.env.NODE_ENV !== 'production'
    ? [myPluginWithSnapshot]
    : []
})

Vuex 自带一个日志插件用于一般的调试:

import { createLogger } from 'vuex'

const store = createStore({
  plugins: [createLogger()]
})

createLogger 函数有几个配置项:

const logger = createLogger({
  collapsed: false, // 自动展开记录的 mutation
  filter (mutation, stateBefore, stateAfter) {
    // 若 mutation 需要被记录,就让它返回 true 即可
    // 顺便,`mutation` 是个 { type, payload } 对象
    return mutation.type !== "aBlocklistedMutation"
  },
  actionFilter (action, state) {
    // 和 `filter` 一样,但是是针对 action 的
    // `action` 的格式是 `{ type, payload }`
    return action.type !== "aBlocklistedAction"
  },
transformer (state) {
    // 在开始记录之前转换状态
    // 例如,只返回指定的子树
    return state.subTree
  },
  mutationTransformer (mutation) {
    // mutation 按照 { type, payload } 格式记录
    // 我们可以按任意方式格式化
    return mutation.type
  },
  actionTransformer (action) {
    // 和 `mutationTransformer` 一样,但是是针对 action 的
    return action.type
  },
  logActions: true, // 记录 action 日志
  logMutations: true, // 记录 mutation 日志
  logger: console, // 自定义 console 实现,默认为 `console`
})

日志插件还可以直接通过 <script> 标签引入,它会提供全局方法 createVuexLogger

要注意,logger 插件会生成状态快照,所以仅在开发环境使用

表单处理

在严格模式中使用vuex时,属于vuex的state上使用v-model会比较麻烦

<input v-model="obj.message">

假设这里的obj是在计算属性中返回的一个属于Vuex store的对象,用户输入时,v-model会试图直接修改obj.message,在严格模式中,由于这个修改不是在mutation函数中执行的(即只能通过mutation修改状态),这里会抛出一个错误

用“Vuex 的思维”去解决这个问题的方法是:给 <input> 中绑定 value,然后侦听 input 或者 change 事件,在事件回调中调用一个方法:

<input :value="message" @input="updateMessage">
// ...
computed: {
  ...mapState({
    message: state => state.obj.message
  })
},
methods: {
  updateMessage (e) {
    this.$store.commit('updateMessage', e.target.value)
  }
}
// ...
mutations: {
  updateMessage (state, message) {
    state.obj.message = message
  }
}

双向绑定的计算属性

必须承认,这样做比简单地使用“v-model + 局部状态”要啰嗦得多,并且也损失了一些 v-model 中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:

<input v-model="message">
// ...
computed: {
  message: {
    get () {
      return this.$store.state.obj.message
    },
    set (value) {
      this.$store.commit('updateMessage', value)
    }
  }
}

测试

主要是针对Vuex中的mutation和action进行单元测试

测试mutation

如果在store.js文件中定义了mutation,并且使用ES2015默认输出了Vuex.Store的实例,那么仍然可以给mutation取变量名然后把它输出去

// mutations.js
export const mutations = {
  increment: state => state.count++
}
// mutations.spec.js
import { expect } from 'chai'
import { mutations } from './store'

// 解构 `mutations`
const { increment } = mutations

describe('mutations', () => {
  it('INCREMENT', () => {
    // 模拟状态
    const state = { count: 0 }
    // 应用 mutation
    increment(state)
    // 断言结果
    expect(state.count).to.equal(1)
  })
})
```p