玩烂 Vuex-阿里云开发者社区

开发者社区> 开发与运维> 正文

玩烂 Vuex

简介: Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex 概念篇

Vuex 是什么?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

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

从软件设计的角度,就是以一种统一的约定和准则,对全局共享状态数据进行管理和操作的设计理念。你必须按照这种设计理念和架构来对你项目里共享状态数据进行CRUD。所以所谓的“状态管理模式”就是一种软件设计的一种架构模式(思想)。

为什么需要这种“状态管理模式”应用到项目中呢?

现如今,流行组件化、模块化开发,多人开发各自组件的时候,不难保证各个组件都是唯一性的,多个组件共享状态肯定是存在的,共享状态又是谁都可以进行操作和修改的,这样就会导致所有对共享状态的操作都是不可预料的,后期出现问题,进行 debug 也是困难重重,往往我们是尽量去避免全局变量。

但大量的业务场景下,不同的模块(组件)之间确实需要共享数据,也需要对其进行修改操作。也就引发软件设计中的矛盾:模块(组件)之间需要共享数据数据可能被任意修改导致不可预料的结果

为了解决其矛盾,软件设计上就提出了一种设计和架构思想,将全局状态进行统一的管理,并且需要获取、修改等操作必须按我设计的套路来。就好比马路上必须遵守的交通规则,右行斑马线就是只能右转一个道理,统一了对全局状态管理的唯一入口,使代码结构清晰、更利于维护。

Vuex 是借鉴了 Flux 、Redux 和 The Elm Architecture 架构模式、设计思想的产物。

ae7aa24f8dc2cad21a18c820938d4a5cbec18a34

什么情况下我应该使用 Vuex?

不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。应用够简单,最好不要使用 Vuex。一个简单的 global event bus (父子组件通信,父组件管理所需的数据状态)就足够您所需了。构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

个人见解,什么时候用?管你小中大型应用,我就想用就用呗,一个长期构建的小型应用项目,谁能知道项目需求以后会是什么样子,毕竟在这浮躁的时代,需求就跟川剧变脸一样快,对不对?毕竟学习了 Vuex 不立马用到项目实战中,你永远不可能揭开 Vuex 的面纱。项目中使用多了,自然而然就会知道什么时候该用上状态管理,什么时候不需要。老话说的好熟能生巧,你认为呢? (括弧 -- 先了解好Vuex 一些基本概念,然后在自己的项目中使用过后,再用到你公司项目上,你别这么虎一上来就给用上去了~)

Vuex 基本使用篇

安装


npm i vuex -S

项目全局中任何地方使用 Vuex, 需要将 Vuex 注册到 Vue 实例中:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

Vuex 上车前的一个

上车规则:

  1. 每一个 Vuex 应用的核心就是 store(仓库),一个项目中必须只有一个 store 实例。包含着你的应用中大部分的状态 (state)。
  2. 不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。

Vuex 和单纯的全局对象有以下两点不同: (1) Vuex 的状态存储是响应式的。Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 (2)不能直接改变 store 中的状态。(重要的事情多来一遍)

上:



<div id="app">
  <p>{{ count }}</p>
  <p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </p>
</div>


import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex) // Vuex 注册到 Vue 中

// 创建一个 store

const store = new Vuex.Store({
  // 初始化 state   
  state: {
    count: 0
  },
 // 改变状态唯一声明处
  mutations: {
  	increment: state => state.count++,
    decrement: state => state.count--
  }
})

new Vue({
  el: '#app',
  // 从根组件将 store 的实例注入到所有的子组件  
  store,  
  computed: {
    count () {
        // Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态,
        // 每当状态 count 发生变化,都会重新求取计算
	    return this.$store.state.count
    }
  },
  methods: {
    increment () {
      this.$store.commit('increment')
    },
    decrement () {
    	this.$store.commit('decrement')
    }
  }
})

state 一个对象管理应用的所有状态,唯一数据源(SSOT, Single source of truth),必须前期初始化就定义好,不然后面在来修改设置 state,程序就不会捕获到 mutations(突变的意思) 所以数据也就不会又任何的更新,组件上也无法体现出来。

获取 store 管理的状态, 因为在 Vue 实例化的时候将 Vuex store 对象 注入了进来 ,所以任何地方都可以通过 this.$store 获取到 store, this.$store.state 来获取状态对象, this.$store.commit 来触发之前定义好的 mutations 中的方法



this.$store.state('count') // => 0

this.$store.commit('increment') // => 1

通过提交 mutation 的方式,而非直接改变 store.state.count, 使用 commit 方式可以让 Vuex 明确地追踪到状态的变化,利于后期维护和调试。

通过了解 state (状态,数据)和 mutations (修改数据唯一声明的地方,类似 SQL 语句)知道了 Vuex 最重要的核心两部分,然后通过掌握 gttter、action、module 来让 Vuex 更加的工程化、合理化来适应更大型的项目的状态管理。

mapState 辅助函数

mapState 可以干什么呢?字面意思状态映射,通过 mapState 可以更加快捷方便帮我们生成计算属性,拿上面的例子进行演示:



 computed: {
    count () {
	    return this.$store.state.count
    }
 }

// 使用 mapState

import { mapState } from 'vuex' // 需要先导入

computed: mapState([
    // 箭头函数方式
	count: state => state.count ,
    
    // or 传字符串参数方式, 'count' 等同于 state => state.count
    countAlias: 'count'
    
    // 获取状态后,你还需要和当前组件别的属性值时,就必须使用常规函数的写法了, 只有这样才能获取到当前组件的 this
    countPlusLocalState (state) {
    	return state.count + this.localCount
    } 
])

当前计算属性名称和状态名称相同时,可以传递一个字符串数组:

computed: mapState([
    // 映射 `this.count` 为 `this.$store.state.count`
	'count'
])

以上使用 mapState 辅助函数后,整个 computed 计算属性都成了 state 状态管理聚集地了, 组件里并不是所有的计算属性都需要被状态管理化,还是有很多计算属性是不需要状态管理的数据的,那如何将它与局部计算属性混合使用呢?

因为 mapState 函数返回的是一个对象。所以我们使用对象扩展运算符就可以把局部计算属性和 mapState 函数返回的对象融合了。



computed: {
	...mapState({
		count: state => state.count    
    }),
    
   localComputed () {
   	   /* ... */ 
   }     
}

️ 注意: 对象扩展运算符,现处于 ECMASCript 提案 stage-4 阶段(将被添加到下一年度发布),所以项目中要使用需要安装 babel-plugin-transform-object-rest-spread 插件 或 安装 presets 环境为 stage 为 1的 env 版本 babel-preset-stage-1 和修改 babelrc 配置文件

.babelrc


{
    "presets": [
        "env",
        "stage-1" // 添加此项
    ],
    "plugins": [
        "transform-vue-jsx",
        "syntax-dynamic-import"
    ]
}

核心概念

上面以讲述过 state 了,这里就不过多的说明。

Getter

Getter 就是 Store 状态管理层中的计算属性,获取源 State 后,希望在对其进行一些包装,再返回给组件中使用。也是就将直接获取到 State 后在 computed 里进行再次的过滤、包装逻辑统统提取出放到 Getter 里进行,提高了代码的复用性、可读性。


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

提取到 Getter

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  //  `Getter` 默认第一个参数为 `state`:  
  getters: {
    doneTodos: state => {
      return state.todos.filter( todo => todo.done )
    }
  }
})


//  组件中获取,通过属性访问

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

️ 注意: getter 在通过属性访问时是作为 Vue 的响应式系统的一部分进行缓存。

Getter 还接受其他 getter 作为第二个参数



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  
    doneTodosLength: (state, getters) => {
    	return getters.doneTodos.length
    }  
  }
})

还可以通过给 Getter 传递参数获取特定的数据



getters: {
	// ...
    
    getTodoById: state => id => {
    	return state.todos.find( todo => todo.id === id )
    }
}

组件内调用方式


this.$store.getters.getTodoById(2) // => { id: 2, text: '...', done: false }

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

mapGetters 辅助函数

和前面 mapState 辅助函数作用和使用上基本相同。


import { mapGetters } from 'vuex'

// getter 名称和 计算属性名称相同的情况下,可以传递字符串数组

export default {
  // ...
  computed: {
  	...mapGetters([
    	'doneTodos'
    ])
  }
}

// 传递对象的方式

export default {
  // ...
  computed: {
  	...mapGetters({
    	doneTodos: 'doneTodos',
        getTodoById: 'getTodoById' // 此处传递回来的是一个函数,所以在使用的时候 => {{ getTodoById(2) }}
    })
  }
}


Mutation

不能直接修改状态,需要通过 Vuex store 中声明的 Mutations 里的方法修改状态。 更改 Vuex 的 store 中的状态的唯一方法是提交 mutationmutation 是一个对象, 含有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。 回调函数就是我们实际进行状态更改的地方,默认接受 state 作为第一个参数。


const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    // increment 事件类型(type)名称,increment() 回调函数
    // increment: function (state) {}  原本写法
    increment (state) {
      // 变更状态
      state.count++
    }
  }
})

mutation handler 不能被直接调用,需要通过 store.commit() 来通知我需要触发一个 mutation handler

this.$store.commit('increment')

mutation 接收参数必须只能两个,超出的都无法获取;第二个参数推荐传递的是一个对象,来接收更多的信息。

this.$store.commit('increment', 10) 

// or

this.$store.commit('increment', { num: 10 }) 

对象风格的提交方式

this.$store.commit({
	type: 'increment',
    num: 10
})

Mutation 需要遵守 Vue 的响应规则

Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:

  1. 最好提前在你的 store 中初始化好所有所需属性(state 中的属性)。
  2. 当需要在对象上添加新属性时,你应该返回的是一个新对象 * 使用 Vue.set(obj, 'newProp', 123),或者 * 以新对象替换老对象。例如: Object.assgin({}, state.obj, newProps) 、对象扩展运算符 state.obj = {...state.obj, newProp: 123 }

mapMutation 辅助函数

使用方式跟 mapStatemapGetters 基本相同。



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

调用方式就跟 methods 其他普通方法一样,通过 this.<methodName> 来调用。

️ 注意: mutation 必须是同步函数,修改 state 必须是同步的、同步的、同步的。 如果是异步的,当触发 mutation 的时候,内部的回调函数还没有被调用,根本不知道实际执行在何处,很难追踪起问题。(实质上任何在回调函数中进行的状态的改变都是不可追踪的。 Vuex 也提供了异步操作的解决方案, 需要将异步操作提取出来放入到 Action 里进行操作。而 Mutation 只负责同步事务

Action

在之前也讲述了,Action 是用来处理异步操作的。这里在详细说明一下 Action 的基本使用。

Action 类似于 mutation, 不同在于:

  • Action 提交的是 mutation,而不是直接变更状态。(不直接修改状态,修改状态还是需要通过 mutation)
  • Action 可以包含任意异步操作。

const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
     // 实践中,经常用到参数解构来简化代码, increment ({commit}) { commit('') }
    increment (context) {
      context.commit('increment')
    }
  }
})

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象(并不是真正的 store 本身),因此可以调用 store.commit 进行提交 mutation, 或者通过 context.statecontext.getters 来获取 stategetters

触发Action

Action 通过 store.dispatch 方法触发:


this.$store.dispatch('increment')

// 以传递额外参数分发
store.dispatch('incrementAsync', {
  amount: 10
})

// 以对象形式分发
store.dispatch({
  type: 'incrementAsync',
  amount: 10
})

与服务器数据异步请求基本在 Action 里进行, 然后通过 Mutation 来同步应用状态state

mapAction 辅助函数

和 mapMutions 使用方式基本一致

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,以处理更加复杂的异步流程?

通过返回一个 Promise 对象来进行组合多个 Action。


actions: {
  actionA ({ commit }) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        commit('someMutation')
        resolve()
      }, 1000)
    })
  }
}

然后:

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

利用 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

由于 Vuex 使用单一状态树模式,来统一管理应用所有的状态,导致所有状态会集中到一个比较大的对象,随着后续不断得迭代,这个对象就会越来越庞大,后期的代码可读性、可维护性就会不断加大。

解决以上问题,就需要对这个对象的内部进行拆分和细分化,对状态进行分门别类,也就产生了模块(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 的状态


声明模块后,state、mutation、action、getter 等使用方式、作用和不在 modules 内声明方式基本一样,只是在细节上进行了一些细微的改变,比如: getter 里默认接收一个参数 state,模块里接收 state 就是本身模块自己的 state 状态了,而不是全局的了; 调用获取上也多一道需要告知那个模块获取状态 等一些细节上的差异。

Module 里 state、mutation、action、getter 上的一些差异

(1)模块内部的 mutation 和 getter,接收的第一个参数 state模块的局部状态对象。 (2)模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState (3)模块内部的 getter,根节点状态会作为第三个参数暴露出来

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应,所以必须防止模块里属性或方法重名。

为了模块具有更高的封装度、复用性和独立性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。在调用上也就需要添加上声明 getter、action 及 mutation 到底属于那个模块了,以路径的形式表示属于那个模块。



const store = new Vuex.Store({
  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']
          }
        }
      }
    }
  }
})

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

带命名空间的模块内访问全局 state 、getter 和 action, rootStaterootGetter会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。

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


modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 全局的 state 和 getters 可以作为第三、四个参数进行传入,从而访问全局 state 和 getters
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> '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'
        }
      }
    }
  }
}

带命名空间的模块里辅助函数如何使用?

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


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

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

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = 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 方法注册模块:


// 注册模块 `myModule`
store.registerModule('myModule', {
  // ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
  // ...
})

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

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

待更新~



原文发布时间为:2018年06月23日
原文作者:Junting
本文来源: 掘金 如需转载请联系原作者

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

分享:
开发与运维
使用钉钉扫一扫加入圈子
+ 订阅

集结各类场景实战经验,助你开发运维畅行无忧

其他文章