引言
vuex是关于对vue中一些多个组件共享的数据进行统一集中式管理的方法。在实际的前端开发中,为了减少重复代码的使用,通常我们会把重复使用的代码,封装成一个组件。不同组件之间一定会存在着数据共享传值的情况。如果数据传值频繁了,会很难控制数据的状态,很难统一协调维护。针对此情况,vue中诞生出了 vuex状态管理工具,react中是redux管理工具。vuex将全局管理共享的数据,统一集中式管理共享数据的状态。
官宣:从vue思想角度考虑,状态管理包含三个部分: state、view、actions。state是驱动应用的数据源,view,以声明方式将state映射到视图,actions,响应在view上的用户输入导致的状态变化。
当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
那么,问题来了,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。
针对上面的问题,vuex中的解决办法:把组件的共享状态抽取出来,以一个全局单例模式管理。在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为。
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这就是vux背后的基本思想。
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。如果开发大型单页应用,使用 Vuex 可能是繁琐冗余的,一个简单的store 模式 (opens new window)就可以满足所需。如果要构建一个中大型单页应用,大有可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。
一个简单的store代码案例,如下
2.不同文件中的内容如下:
Hello-world.vue
<template> <div class="hello"> <h1>{{ msg }}</h1> <h1>应用store中的 count {{ num }}</h1> <p> For a guide and recipes on how to configure / customize this project,<br> check out the <a href="https://cli.vuejs.org" target="_blank" >vue-cli documentation</a>. </p> <h3>Installed CLI Plugins</h3> <ul> <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" >babel</a></li> <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" >eslint</a></li> </ul> </div> </template> <script> export default { name: 'HelloWorld', props: { msg: String }, data(){ return { num:this.$store.state.count } } } </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> h3 { margin: 40px 0 0; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } </style>
App.vue
<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png" alt="vue中的共享数据管理vuex"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'App', components: { HelloWorld } } </script> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } </style>
main.js
import Vue from 'vue' import App from './App.vue' import store from './store' Vue.config.productionTip = false new Vue({ render: h => h(App), store, components: { App }, template: '<App/>' }).$mount('#app')
store.js
import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) let store = new Vuex.Store({ state:{ count:1 } }) export default store
Vuex使用单一状态树,用一个状态包含了全部的应用层级状态。每个应用仅仅包含一个store实例。单一状态树可以支持定位任一特定的状态片段,在调试过程中也能容易获取整个当前应用状态的快照。
const counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } }
每当 store.state.count 变化的时候,都会重新求取计算属性,并且触发更新相关联的DOM。这种模式导致组件全局状态单例,在模块化的构建系统中,在每个需要使用state的组件中需要频繁导入,并且在测试组件时需要模拟状态。
Vuex通过 store选项,提供了一种机制将状态从根组件注入到每一个子组件中 Vue.use(Vuex)
const app = new Vue({ el: '#app', // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件 store, components: { Counter }, template: ` <div class="app"> <counter></counter> </div> ` })
通过在根实例注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过this.$store访问到。让我们更新下Counter的实现
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return this.$store.state.count } } }
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗杂。
// 在单独构建的版本中辅助函数为 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 } }) }
当映射的计算属性的名称与state的子节点名称相同时,我们可以给mapState传一个字符串数组。
computed: mapState([ // 映射 this.count 为 store.state.count 'count' ])
对象展开运算符mapstate函数返回的是一个对象。我们如何将它和局部计算属性混合使用?通常我们需要需要一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed属性。对象展开符可以简化这个写法
computed: { localComputed(){ //... }, ...mapstate({ //... }) }
组件仍可以具有本地状态使用Vuex并不意味着您将所有状态放入Vuex中。如果一个状态属于一个单一组件,那么将其保留为本地状态就可以了。在实际开发中,可以权衡是加入全局的Vuex中,还是单个组件的Vuex中。
有时我们可能要根据存储状态来计算派生状态,例如,通过项目列表进行过滤并进行计数。
computed: { doneTodosCount(){ return this.$store.state.todo.filter(todo => todo.done).length } }
如果在多个组件中使用上面的状态函数,还需要复制多分放在不同的组件中。针对此,Vuex允许我们在store中定义 getters.,可以将其视为 store的计算属性,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 将其暴露在store.getters 对象上,可以作为属性直接访问
store.getters.doneTodos
getters还将接收其他getters作为第二个参数
getter: { doneTodosCount:(state,getters) => { return getters.doneTodos.length } }
现在,我们可以在任何组件中使用它
computed: { doneTodosCount () { return this.$store.getters.doneTodosCount } }
注意: 作为属性访问的getters在vuex生态系统中被缓存
还可以通过返回函数将参数传递给 getter。当需要查询存储中的数组时,这特别有用
getters: { getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } } store.getters.getTodoById(2)
mapGetters助手可以简单地将 store中的 getters映射到本地计算属性中
import { mapGetters } from 'vuex'; export default { computed: { ...mapGetters([ 'doneTodosCount', 'anotherGetter' ]) } }
你还可以将 一个getter映射为不同的名字,当做一个对象
...mapGetters({ doneCount:'doneTodosCount' })
其实在Vuex store中,改变变化的状态,唯一的方法是通过传递一个mutations。Vuex mutations和事件非常相似:每一个mutation有一个type和一个handler。在handler函数中,可以操作实际的state变更,并且它接收这个state作为第一个参数
const store = new Vuex.Store({ state: { count:1 }, mutations: { increment(state){ state.count ++ } } })
不能直接调用一个mutation handler。它看起来更像是事件注册。当一个mutation作为type increment被触发,这个handler被调用。为了触发一个mutation handler,你应该调用store.commit作为它的type
store.commit('increment')
Commit with Payload
可以向store.commit 传入额外的参数,即mutation的载荷。在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读:
mutations:{ increment(state,n){ state.count+=n; } } store.commit('increment',10)
Object-Style Commit
提交mutation的另一种方式是直接使用包含 type 属性的对象
store.commit({ type:'increment', amount: 10 })
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:
mutations: { increment(state,payload){ state.count+=payload.amount } }
Mutation 需遵守 Vue 的响应规则
既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
最好提前在你的 store 中初始化好所有所需属性。
当需要在对象上添加新属性时,你应该
使用Vue.set(obj, 'newProp', 123)
, 或者
以新对象替换老对象。例如,利用对象展开运算符 (opens new window)我们可以这样写:
state.obj = { ...state.obj, newProp:123 }
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然。在需要多人协作的大型项目中,这会很有帮助。
// 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: { // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名 [SOME_MUTATION] (state) { // mutate state } } })
Mutation 必须是同步函数
一条重要的原则就是要记住mutation 必须是同步函数。
mutations: { someMutation(state){ api.callAsyncMethod(() => { state.count++ }) } }
任何在回调函数中进行的状态的改变都是不可追踪的。
在组件中提交 Mutation
可以在组件中使用this.$store.commit('xxx')
提交 mutation,或者使用mapMutations
辅助函数将组件中的 methods 映射为store.commit
调用
import { mapMutations } from 'vuex' export default { ...mapMutations([ 'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')` 'incrementBy' // 将 `this.incrementBy(amount)` 映射为`this.$store.commit('incrementBy', amount)` ]), ...mapMutations([ add:'increment' ]) }
在 mutation 中混合异步调用会导致你的程序很难调试。例如,当你调用了两个包含异步回调的 mutation 来改变状态,你怎么知道什么时候回调和哪个先回调呢?在 Vuex 中,mutation 都是同步事务:
Action 类似于 mutation,不同在于:
const store = new Vuex.Store({ 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。
实践中,我们会经常用到 ES2015 的参数解构 (opens new window)来简化代码(特别是我们需要调用commit
很多次的时候):(不太懂)
actions: { increment({commit}){ commit('increment') } }
Action可以通过 store.dispatch 方法触发
store.dispatch('increment')
在action内部可以执行异步可以执行异步操作
actions: { incrementAsync({commit}){ setTimeout(() => { commit('increment') },1000) } }
Actions 支持同样的载荷方式和对象方式进行分发:
store.dispathch('incrementAsync',{ amount:10 }) store.dispatch({ type:'incrementAsybc', amount: 10 })
复杂案例:涉及到调用异步 API 和分发多重 mutation:
actions:{ checkout({ commit, state },products) { const savedCartItems = [...state.cart.added]; commit(types.CHECKOUT_REQUEST) shop.buyProducts( products, () => commit(types.CHECKOUT_SUCCESS) () => commit(types.CHECKOUT_FAILURE,savedCartItems) ) } }
在组件中使用this.$store.dispatch('xxx')
分发 action,或者使用mapActions
辅助函数将组件的 methods 映射为store.dispatch
调用(需要先在根节点注入store
)
import { mapActions } from 'vuex' export default { methods: { ...mapActions([ 'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')` 'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)` ]), ...mapActions([ add:'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')` ]) } }
Action 通常是异步的,action 结束后,处理异步流程。组合多个action,处理更加复杂的异步流程。
store.dispatch
可以处理被触发的 action 的处理函数返回的 Promise,并且store.dispatch
仍旧返回 Promise:
actions: { actionA ({ commit }) { return new Promise((resolve,reject) => { setTimeout(() => { commit('someMutation') },1000) }) } } store.dispatch('actionA').then(() => {}) ///或者 actions: { actionB({ dispatch,commit }) { return dispatch('actionA').then(() => { commit('someOtherMutation') }) } }
最后,如果我们利用async / await (opens new window),我们可以如下组合 action:
actions: { aync actionA({ commit }){ commit('goData',await getData()) }, async actionB ({ dispatch, commit }){ await dispatch('actionA') commit('goOtherData', await getOtherData()) } }
一个store.dispatch
在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(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 的状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
const moduleA = { state: () => ({ count: 0 }), mutations: { increment (state) { // 这里的 `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 } } }
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:
const store = new Vuex.Store({ modules: { acccount: { namespace: true, state: () => ({}), 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 会收到局部化的getter
,dispatch
和commit
。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改namespaced
属性后不需要修改模块内的代码。
如果你希望使用全局 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/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,你可添加root: true
,并将这个 action 的定义放在函数handler
中。例如:
{ actions: { someOtherAction ({dispatch}) { dispatch('someAction') } }, modules: { foo: { namespaced: true, actions: { someAction: { root: true, handler (namespacedContext, payload) { ... } // -> 'someAction' } } } } }
当使用mapState
,mapGetters
,mapActions
和mapMutations
这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:
computed: { ...mapState({ a: state => state.some.nested.module.a, b: state => state.some.nested.module.b }) }, methods: { ...mapActions([ 'some/nested/module/foo', // -> this['some/nested/module/foo']() 'some/nested/module/bar' // -> this['some/nested/module/bar']() ]) }
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。
computed: { ...mapState('some/nested/module',{ a: state => state.a, b: state => state.b }) }, method: { ...mapActions('some/nested/module',{ 'foo', 'bar' }) }
可以通过使用createNamespacedHelpers
创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
import { createNamespacedHelpers } from 'vuex' const { mapState, mapAction } = createNamespacecdHelper('some/nested/module') export default { computed: { ...mapState({ a: state => state.a, b: state => state.b }) }, method: { ...mapActions([ 'foo', 'bar' ]) } }
如果你开发的插件(Plugin)提供了模块并允许用户将其添加到 Vuex store,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称
export function createPlugin(options ={}){ return function(store){ const namespace = options.namespace || ''; store.dispatch(namespace + 'pluginAction') } }
在 store 创建之后,你可以使用store.registerModule
方法注册模块:
import Vuex from 'vuex' const store = new Vuex.Store({}) // 注册模块 `myModule` store.registerModule('myModule',{}) // 注册嵌套模块 `nested/myModule` store.registerModule(['nested','myNodule'],{})
之后就可以通过store.state.myModule
和store.state.nested.myModule
访问模块的状态。
模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync
(opens new window)插件就是通过动态注册模块将 vue-router 和 vuex 结合在一起,实现应用的路由状态管理。
使用store.unregisterModule(moduleName)
来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。
可以通过store.hasModule(moduleName)
方法检查该模块是否已经被注册到 store
在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过preserveState
选项将其归档:store.registerModule('a', module, { preserveState: true })
。
当你设置preserveState: true
时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。
有时我们可能需要创建一个模块的多个实例
runInNewContext
选项是false
或'once'
时,为了在服务端渲染中避免有状态的单例 (opens new window))如果使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。
实际上这和 Vue 组件内的data
是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持)
const MyReusableModule = { state: ()=> { foo:'bar' }, }
Vuex不限制代码结构。但是,它规定了一些需要遵守的原则
如果 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
store使用插件
const store = new Vuex.Store({ // ... plugins: [myPlugin] })
Vuex 的 store 接受plugins
选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:
在插件中不允许直接修改状态——类似于组件,只能通过提交 mutation 来触发变化。
通过提交 mutation,插件可以用来同步数据源到 store。例如,同步 websocket 数据源到 store(下面是个大概例子,实际上createPlugin
方法可以有更多选项来完成复杂任务)
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 = new Vuex.Store({ state, mutations, plugins: [plugin] })
有时候插件需要获得状态的“快照”,比较改变的前后状态。想要实现这项功能,你需要对状态对象进行深拷贝:
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 = new Vuex.Store({ plugins: process.env.NODE_ENV !== 'production' ? [myPluginWidthSnapshot] : [] })
上面插件会默认启用。在发布阶段,你需要使用 webpack 的DefinePlugin (opens new window)或者是 Browserify 的envify (opens new window)使process.env.NODE_ENV !== 'production'
为false
。
Vuex 自带一个日志插件用于一般的调试:
import createLogger from 'vuex/dist/logger' const store = new Vuex.Store({ plugins:[createLogger] }) const logger = createLogger({ collapsed: false, filter(mutation, stateBefore, stateAfter){ return mutation.type !== 'BlocklistedMutation' }, 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` })