Vue的状态管理vuex使用方法详解
引入vuex
当访问数据对象时,一个Vue实例只是简单的代理访问。所以,如果有一处需要被多个实例间共享的状态,可以简单地通过维护一份数据来实现共享
constsourceOfTruth={} constvmA=newVue({ data:sourceOfTruth }) constvmB=newVue({ data:sourceOfTruth })
现在当sourceOfTruth发生变化,vmA和vmB都将自动的更新引用它们的视图。子组件们的每个实例也会通过this.$root.$data去访问。现在有了唯一的实际来源,但是,调试将会变为噩梦。任何时间,应用中的任何部分,在任何数据改变后,都不会留下变更过的记录。
为了解决这个问题,采用一个简单的store模式:
varstore={ debug:true, state:{ message:'Hello!' }, setMessageAction(newValue){ if(this.debug)console.log('setMessageActiontriggeredwith',newValue) this.state.message=newValue }, clearMessageAction(){ if(this.debug)console.log('clearMessageActiontriggered') this.state.message='' } }
所有store中state的改变,都放置在store自身的action中去管理。这种集中式状态管理能够被更容易地理解哪种类型的mutation将会发生,以及它们是如何被触发。当错误出现时,现在也会有一个log记录bug之前发生了什么
此外,每个实例/组件仍然可以拥有和管理自己的私有状态:
varvmA=newVue({ data:{ privateState:{}, sharedState:store.state } }) varvmB=newVue({ data:{ privateState:{}, sharedState:store.state } })
[注意]不应该在action中替换原始的状态对象,组件和store需要引用同一个共享对象,mutation才能够被观察
接着继续延伸约定,组件不允许直接修改属于store实例的state,而应执行action来分发(dispatch)事件通知store去改变,最终达成了Flux架构。这样约定的好处是,能够记录所有store中发生的state改变,同时实现能做到记录变更(mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具
Vuex概述
Vuex是一个专为Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex提供了诸如零配置的time-travel调试、状态快照导入导出等高级调试功能
状态管理模式
下面以一个简单的计数应用为例,来说明状态管理模式
newVue({ el:'#app', //state data(){ return{ count:0 } }, //view template:`{{count}}`, //actions methods:{ increment(){ this.count++ } } })
这个状态自管理应用包含以下几个部分:
state,驱动应用的数据源;
view,以声明方式将state映射到视图;
actions,响应在view上的用户输入导致的状态变化。
下面是一个表示“单向数据流”理念的极简示意:
但是,当应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏,存在以下两个问题
1、多个视图依赖于同一状态
2、来自不同视图的行为需要变更同一状态
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为
另外,通过定义和隔离状态管理中的各种概念并强制遵守一定的规则,代码将会变得更结构化且易维护。
这就是Vuex背后的基本思想,借鉴了Flux、Redux、和TheElmArchitecture。与其他模式不同的是,Vuex是专门为Vue.js设计的状态管理库,以利用Vue.js的细粒度数据响应机制来进行高效的状态更新
Vuex使用情况
虽然Vuex可以帮助管理共享状态,但也附带了更多的概念和框架。这需要对短期和长期效益进行权衡。
如果不打算开发大型单页应用,使用Vuex可能是繁琐冗余的。确实是如此——如果应用够简单,最好不要使用Vuex。一个简单的globaleventbus就足够所需了。但是,如果需要构建是一个中大型单页应用,很可能会考虑如何更好地在组件外部管理状态,Vuex将会成为自然而然的选择
开始
安装Vuex
npminstallvuex--save
在一个模块化的打包系统中,必须显式地通过Vue.use()来安装Vuex
importVuefrom'vue' importVuexfrom'vuex' Vue.use(Vuex)
当使用全局script标签引用Vuex时,不需要以上安装过程
概述
每一个Vuex应用的核心就是store(仓库)。“store”基本上就是一个容器,它包含着应用中大部分的状态(state)。Vuex和单纯的全局对象有以下两点不同:
1、Vuex的状态存储是响应式的。当Vue组件从store中读取状态的时候,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新
2、不能直接改变store中的状态。改变store中的状态的唯一途径就是显式地提交(commit)mutation。这样使得可以方便地跟踪每一个状态的变化,从而能够实现一些工具帮助更好地了解应用
最简单的store
下面来创建一个store。创建过程直截了当——仅需要提供一个初始state对象和一些mutation
//如果在模块化构建系统中,请确保在开头调用了Vue.use(Vuex) conststore=newVuex.Store({ state:{ count:0 }, mutations:{ increment(state){ state.count++ } } })
现在,可以通过store.state来获取状态对象,以及通过store.commit方法触发状态变更:
store.commit('increment') console.log(store.state.count)//->1
通过提交mutation的方式,而非直接改变store.state.count,是因为想要更明确地追踪到状态的变化。这个简单的约定能够让意图更加明显,这样在阅读代码的时候能更容易地解读应用内部的状态改变。此外,这样也有机会去实现一些能记录每次状态改变,保存状态快照的调试工具。有了它,甚至可以实现如时间穿梭般的调试体验。
由于store中的状态是响应式的,在组件中调用store中的状态简单到仅需要在计算属性中返回即可。触发变化也仅仅是在组件的methods中提交mutation
下面是一个使用vuex实现的简单计数器
conststore=newVuex.Store({ state:{ count:0 }, mutations:{ increment:state=>state.count++, decrement:state=>state.count--, } }) newVue({ el:'#app', computed:{ count(){ returnstore.state.count } }, //view template:``, //actions methods:{ increment(){ store.commit('increment') }, decrement(){ store.commit('decrement') }, } }){{count}}
核心概念
state
单一状态树
Vuex使用单一状态树——用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源(SSOT)”而存在。这也意味着,每个应用将仅仅包含一个store实例。单一状态树能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照
在VUE组件中获得VUEX状态
如何在Vue组件中展示状态呢?由于Vuex的状态存储是响应式的,从store实例中读取状态最简单的方法就是在计算属性中返回某个状态
//创建一个Counter组件 constCounter={ template:`{{count}}`, computed:{ count(){ returnstore.state.count } } }
每当store.state.count变化的时候,都会重新求取计算属性,并且触发更新相关联的DOM
然而,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用state的组件中需要频繁地导入,并且在测试组件时需要模拟状态。
Vuex通过store选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用Vue.use(Vuex)):
constapp=newVue({ el:'#app', //把store对象提供给“store”选项,这可以把store的实例注入所有的子组件 store, components:{Counter}, template:`
通过在根实例中注册store选项,该store实例会注入到根组件下的所有子组件中,且子组件能通过this.$store访问到。下面来更新下Counter的实现:
constCounter={ template:`{{count}}`, computed:{ count(){ returnthis.$store.state.count } } }
mapState辅助函数
当一个组件需要获取多个状态时,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,可以使用mapState辅助函数帮助生成计算属性
//在单独构建的版本中辅助函数为Vuex.mapState import{mapState}from'vuex' exportdefault{ //... computed:mapState({ //箭头函数可使代码更简练 count:state=>state.count, //传字符串参数'count'等同于`state=>state.count` countAlias:'count', //为了能够使用`this`获取局部状态,必须使用常规函数 countPlusLocalState(state){ returnstate.count+this.localCount } }) }
当映射的计算属性的名称与state的子节点名称相同时,也可以给mapState传一个字符串数组
computed:mapState([ //映射this.count为store.state.count 'count' ])
对象展开运算符
mapState函数返回的是一个对象。如何将它与局部计算属性混合使用呢?通常,需要使用一个工具函数将多个对象合并为一个,将最终对象传给computed属性。但是自从有了对象展开运算符,可以极大地简化写法:
computed:{ localComputed(){/*...*/}, //使用对象展开运算符将此对象混入到外部对象中 ...mapState({ //... }) }
组件仍然保有局部状态
使用Vuex并不意味着需要将所有的状态放入Vuex。虽然将所有的状态放到Vuex会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态
Getter
有时候需要从store中的state中派生出一些状态,例如对列表进行过滤并计数:
computed:{ doneTodosCount(){ returnthis.$store.state.todos.filter(todo=>todo.done).length } }
如果有多个组件需要用到此属性,要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想
Vuex允许在store中定义“getter”(可以认为是store的计算属性)。就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算
Getter接受state作为其第一个参数:
conststore=newVuex.Store({ state:{ todos:[ {id:1,text:'...',done:true}, {id:2,text:'...',done:false} ] }, getters:{ doneTodos:state=>{ returnstate.todos.filter(todo=>todo.done) } } })
Getter会暴露为store.getters对象:
store.getters.doneTodos//->[{id:1,text:'...',done:true}]
Getter也可以接受其他getter作为第二个参数:
getters:{ //... doneTodosCount:(state,getters)=>{ returngetters.doneTodos.length } } store.getters.doneTodosCount//->1
可以很容易地在任何组件中使用它:
computed:{ doneTodosCount(){ returnthis.$store.getters.doneTodosCount } }
也可以通过让getter返回一个函数,来实现给getter传参。在对store里的数组进行查询时非常有用
getters:{ //... getTodoById:(state,getters)=>(id)=>{ returnstate.todos.find(todo=>todo.id===id) } } store.getters.getTodoById(2)//->{id:2,text:'...',done:false}
如果箭头函数不好理解,翻译成普通函数如下
vargetTodoById=function(state,getters){ returnfunction(id){ returnstate.todos.find(function(todo){ returntodo.id===id }) } } store.getters.getTodoById(2)//->{id:2,text:'...',done:false}
mapGetters辅助函数
mapGetters辅助函数仅仅是将store中的getter映射到局部计算属性:
import{mapGetters}from'vuex' exportdefault{ //... computed:{ //使用对象展开运算符将getter混入computed对象中 ...mapGetters([ 'doneTodosCount', 'anotherGetter', //... ]) } }
如果想将一个getter属性另取一个名字,使用对象形式:
mapGetters({ //映射`this.doneCount`为`store.getters.doneTodosCount` doneCount:'doneTodosCount' })
mutation
更改Vuex的store中的状态的唯一方法是提交mutation。Vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是实际进行状态更改的地方,并且它会接受state作为第一个参数:
conststore=newVuex.Store({ state:{ count:1 }, mutations:{ increment(state){ //变更状态 state.count++ } } })
不能直接调用一个mutationhandler。这个选项更像是事件注册:“当触发一个类型为increment的mutation时,调用此函数。”要唤醒一个mutationhandler,需要以相应的type调用store.commit方法:
store.commit('increment')
提交载荷(Payload)
可以向store.commit传入额外的参数,即mutation的载荷(payload)
//... 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函数,因此handler保持不变:
mutations:{ increment(state,payload){ state.count+=payload.amount } }
遵守响应规则
既然Vuex的store中的状态是响应式的,那么当变更状态时,监视状态的Vue组件也会自动更新。这也意味着Vuex中的mutation也需要与使用Vue一样遵守一些注意事项:
1、最好提前在store中初始化好所有所需属性
2、当需要在对象上添加新属性时,应该使用Vue.set(obj,'newProp',123),或者以新对象替换老对象
例如,利用对象展开运算符可以这样写:
state.obj={...state.obj,newProp:123}
使用常量替代Mutation事件类型
使用常量替代mutation事件类型在各种Flux实现中是很常见的模式。这样可以使linter之类的工具发挥作用,同时把这些常量放在单独的文件中可以让代码合作者对整个app包含的mutation一目了然
//mutation-types.js exportconstSOME_MUTATION='SOME_MUTATION' //store.js importVuexfrom'vuex' import{SOME_MUTATION}from'./mutation-types' conststore=newVuex.Store({ state:{...}, mutations:{ //可以使用ES2015风格的计算属性命名功能来使用一个常量作为函数名 [SOME_MUTATION](state){ //mutatestate } } })
Mutation必须是同步函数
一条重要的原则就是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' exportdefault{ //... 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')` }) } }
action
在mutation中混合异步调用会导致程序很难调试。例如,当能调用了两个包含异步回调的mutation来改变状态,怎么知道什么时候回调和哪个先回调呢?这就是为什么要区分这两个概念。在Vuex中,mutation都是同步事务:
store.commit('increment') //任何由"increment"导致的状态变更都应该在此刻完成。
Action类似于mutation,不同之处在于:
1、Action提交的是mutation,而不是直接变更状态
2、Action可以包含任意异步操作
下面来注册一个简单的action
conststore=newVuex.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的参数解构来简化代码(特别是需要调用commit很多次的时候)
actions:{ increment({commit}){ commit('increment') } }
分发Action
Action通过store.dispatch方法触发
store.dispatch('increment')
乍一眼看上去感觉多此一举,直接分发mutation岂不更方便?实际上并非如此,mutation必须同步执行这个限制,而Action就不受约束,可以在action内部执行异步操作
actions:{ incrementAsync({commit}){ setTimeout(()=>{ commit('increment') },1000) } }
Actions支持同样的载荷方式和对象方式进行分发
//以载荷形式分发 store.dispatch('incrementAsync',{ amount:10 }) //以对象形式分发 store.dispatch({ type:'incrementAsync', amount:10 })
来看一个更加实际的购物车示例,涉及到调用异步API和分发多重mutation:
actions:{ checkout({commit,state},products){ //把当前购物车的物品备份起来 constsavedCartItems=[...state.cart.added] //发出结账请求,然后乐观地清空购物车 commit(types.CHECKOUT_REQUEST) //购物API接受一个成功回调和一个失败回调 shop.buyProducts( products, //成功操作 ()=>commit(types.CHECKOUT_SUCCESS), //失败操作 ()=>commit(types.CHECKOUT_FAILURE,savedCartItems) ) } }
注意正在进行一系列的异步操作,并且通过提交mutation来记录action产生的副作用(即状态变更)
在组件中分发Action
在组件中使用this.$store.dispatch('xxx')分发action,或者使用mapActions辅助函数将组件的methods映射为store.dispatch调用(需要先在根节点注入store):
import{mapActions}from'vuex' exportdefault{ //... 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.dispatch可以处理被触发的action的处理函数返回的Promise,并且store.dispatch仍旧返回Promise:
actions:{ actionA({commit}){ returnnewPromise((resolve,reject)=>{ setTimeout(()=>{ commit('someMutation') resolve() },1000) }) } }
现在可以
store.dispatch('actionA').then(()=>{ //... })
在另外一个action中也可以:
actions:{ //... actionB({dispatch,commit}){ returndispatch('actionA').then(()=>{ commit('someOtherMutation') }) } }
最后,如果利用async/await这个JavaScript新特性,可以像这样组合action:
//假设getData()和getOtherData()返回的是Promise actions:{ asyncactionA({commit}){ commit('gotData',awaitgetData()) }, asyncactionB({dispatch,commit}){ awaitdispatch('actionA')//等待actionA完成 commit('gotOtherData',awaitgetOtherData()) } }
一个store.dispatch在不同模块中可以触发多个action函数。在这种情况下,只有当所有触发函数完成后,返回的Promise才会执行
module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。
为了解决以上问题,Vuex允许将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
constmoduleA={ state:{...}, mutations:{...}, actions:{...}, getters:{...} } constmoduleB={ state:{...}, mutations:{...}, actions:{...} } conststore=newVuex.Store({ modules:{ a:moduleA, b:moduleB } }) store.state.a//->moduleA的状态 store.state.b//->moduleB的状态
模块的局部状态
对于模块内部的mutation和getter,接收的第一个参数是模块的局部状态对象
constmoduleA={ state:{count:0}, mutations:{ increment(state){ //这里的`state`对象是模块的局部状态 state.count++ } }, getters:{ doubleCount(state){ returnstate.count*2 } } }
同样,对于模块内部的action,局部状态通过context.state暴露出来,根节点状态则为context.rootState:
constmoduleA={ //... actions:{ incrementIfOddOnRootSum({state,commit,rootState}){ if((state.count+rootState.count)%2===1){ commit('increment') } } } }
对于模块内部的getter,根节点状态会作为第三个参数暴露出来:
constmoduleA={ //... getters:{ sumWithRootCount(state,getters,rootState){ returnstate.count+rootState.count } } }
命名空间
默认情况下,模块内部的action、mutation和getter是注册在全局命名空间的——这样使得多个模块能够对同一mutation或action作出响应
如果希望模块具有更高的封装度和复用性,可以通过添加namespaced:true的方式使其成为命名空间模块。当模块被注册后,它的所有getter、action及mutation都会自动根据模块注册的路径调整命名。例如:
conststore=newVuex.Store({ modules:{ account:{ namespaced:true, //模块内容(moduleassets) 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会收到局部化的getter,dispatch和commit。换言之,在使用模块内容(moduleassets)时不需要在同一模块内额外添加空间名前缀。更改namespaced属性后不需要修改模块内的代码
在命名空间模块内访问全局内容(GlobalAssets)
如果希望使用全局state和getter,rootState和rootGetter会作为第三和第四参数传入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){...} } } }
带命名空间的绑定函数
当使用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', 'some/nested/module/bar' ]) }
对于这种情况,可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为
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') exportdefault{ computed:{ //在`some/nested/module`中查找 ...mapState({ a:state=>state.a, b:state=>state.b }) }, methods:{ //在`some/nested/module`中查找 ...mapActions([ 'foo', 'bar' ]) } }
注意事项
如果开发的插件(Plugin)提供了模块并允许用户将其添加到Vuexstore,可能需要考虑模块的空间名称问题。对于这种情况,可以通过插件的参数对象来允许用户指定空间名称:
//通过插件的参数对象得到空间名称 //然后返回Vuex插件函数 exportfunctioncreatePlugin(options={}){ returnfunction(store){ //把空间名字添加到插件模块的类型(type)中去 constnamespace=options.namespace||'' store.dispatch(namespace+'pluginAction') } }
模块动态注册
在store创建之后,可以使用store.registerModule方法注册模块:
//注册模块`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时声明的模块)
模块重用
有时可能需要创建一个模块的多个实例,例如:
1、创建多个store,他们公用同一个模块(例如当runInNewContext选项是false或'once'时,为了在服务端渲染中避免有状态的单例)
2、在一个store中多次注册同一个模块
如果使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时store或模块间数据互相污染的问题。
实际上这和Vue组件内的data是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅2.3.0+支持):
constMyReusableModule={ state(){ return{ foo:'bar' } }, //mutation,action和getter等等... }
项目结构
Vuex并不限制代码结构。但是,它规定了一些需要遵守的规则:
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 #产品模块
插件
Vuex的store接受plugins选项,这个选项暴露出每次mutation的钩子。Vuex插件就是一个函数,它接收store作为唯一参数:
constmyPlugin=store=>{ //当store初始化后调用 store.subscribe((mutation,state)=>{ //每次mutation之后调用 //mutation的格式为{type,payload} }) }
然后像这样使用:
conststore=newVuex.Store({ //... plugins:[myPlugin] })
在插件中提交Mutation
在插件中不允许直接修改状态——类似于组件,只能通过提交mutation来触发变化。
通过提交mutation,插件可以用来同步数据源到store。例如,同步websocket数据源到store(下面是个大概例子,实际上createPlugin方法可以有更多选项来完成复杂任务):
exportdefaultfunctioncreateWebSocketPlugin(socket){ returnstore=>{ socket.on('data',data=>{ store.commit('receiveData',data) }) store.subscribe(mutation=>{ if(mutation.type==='UPDATE_DATA'){ socket.emit('update',mutation.payload) } }) } } constplugin=createWebSocketPlugin(socket) conststore=newVuex.Store({ state, mutations, plugins:[plugin] })
生成State快照
有时候插件需要获得状态的“快照”,比较改变的前后状态。想要实现这项功能,需要对状态对象进行深拷贝:
constmyPluginWithSnapshot=store=>{ letprevState=_.cloneDeep(store.state) store.subscribe((mutation,state)=>{ letnextState=_.cloneDeep(state) //比较prevState和nextState... //保存状态,用于下一次mutation prevState=nextState }) }
生成状态快照的插件应该只在开发阶段使用,使用webpack或Browserify,让构建工具帮助处理:
conststore=newVuex.Store({ //... plugins:process.env.NODE_ENV!=='production' ?[myPluginWithSnapshot] :[] })
上面插件会默认启用。在发布阶段,需要使用webpack的DefinePlugin或者是Browserify的envify使process.env.NODE_ENV!=='production'为false
内置Logger插件
Vuex自带一个日志插件用于一般的调试:
importcreateLoggerfrom'vuex/dist/logger' conststore=newVuex.Store({ plugins:[createLogger()] }) createLogger函数有几个配置项: constlogger=createLogger({ collapsed:false,//自动展开记录的mutation filter(mutation,stateBefore,stateAfter){ //若mutation需要被记录,就让它返回true即可 //顺便,`mutation`是个{type,payload}对象 returnmutation.type!=="aBlacklistedMutation" }, transformer(state){ //在开始记录之前转换状态 //例如,只返回指定的子树 returnstate.subTree }, mutationTransformer(mutation){ //mutation按照{type,payload}格式记录 //我们可以按任意方式格式化 returnmutation.type } })
日志插件还可以直接通过
更多关于Vue的状态管理vuex使用方法请点击下面的相关链接
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。