一.出发点
在相对独立的组件中,action -> state -> view
的单向数据流能得到保证。而真实业务场景经常需要状态传递及共享,一般方法是:
状态传递:父子组件通信通过
props
完成(正向传属性值,反向传方法),对于兄弟组件间通信,则需要通过事件或者把状态提升到父级(把兄弟通信问题转换成父子通信)来完成状态共享:要么放在一个组件里,其它组件想办法拿到状态引用,要么提出来作为单例,供各组件共享
深层次的props
传递比较难受,兄弟组件间的交错的事件通信会带来维护上的问题,提升状态到父级会让父级膨胀,管理过多细节状态。把共享状态放在一个组件里,其它组件取状态引用比较费劲,提出来作为单例稍好一些,但组件树外存在零散的共享状态,也可能会带来维护上的问题
把状态层单独提出来,能有效解决状态传递和共享的问题,再用action
给状态变更添上语义,不仅缓解了维护上的问题,还带来了调试方面的好处
二.基本原则
应用级的状态由
store
集中管理修改状态的唯一方式是
commit
同步的mutation
异步逻辑放在
action
里
认同便于管理的单一状态树、规范修改状态的方式,此外更贴近业务,从设计上考虑异步场景
三.结构
不像Redux一样奇怪(reducer
乍看好像和Flux没什么关系),Vuex更像是中规中矩的Flux实现:
component 视图层 dispatch action
---
action 事件层 commit mutation
异步 统一管理异步请求
---
mutation 响应层 mutate state
同步 逻辑上原子级的状态修改
---
state 数据模型层 update model
通过 数据绑定 映射到视图更新
其中,mutation, action
都是全局共享的,所以也解决了组件通信的问题(不需要手动传递状态,只需要告诉store
发生了什么,store
知道该做什么),避免提升/传递状态,并带来了语义上的好处
全局共享就存在命名冲突的问题,所以Vuex还提供了命名空间选项
对比Flux
产生action 传递action update state
view交互 -----------> dispatcher -----------> stores --------------> views
可以发现最大的区别是Vuex把action
细分成了action
和mutation
,分别应对异步场景和同步场景,由store
自身充当dispatcher
(负责注册/分发action/(mutation)
)
也就是说,把action, mutation
看作一层(Flux里的action
)的话,二者结构完全一致,所以说Vuex更像是中规中矩的Flux实现
store
作为state
的容器,另外充当dispatcher
用store
来管理state
,从作用上看相当于global.share = {}
,但Vuex里的store.state
有一些别的特点:
state
是响应式数据不允许直接修改
store
持有的state
,必须显式的commit mutation
与组件的data
类似,store.state
也是响应式的,与组件的计算属性关联起来,state
更新精确传递到view
层
而不允许直接修改store.state
也是道德约束,虽然在开启strict
选项后会报错,而实际上修改是可以生效的,这里不做强约束(写保护)可能是出于市场考虑
另外:
单一状态树,与Redux相同,提供额外模块化机制来管理(拆分/组织
state
)同样,不要求把所有
state
全都塞进Vuex,建议把相对独立的维护在组件级
getter
作用上相当于store
的计算属性
用来包装state
,把原始state
包装(对store.state
做简单计算,比如filter, count, find
等等)成视图展示需要的形式
没有getter
的话,这部分弱逻辑要么放在computed
里,要么放在模版里,提供getter
把state
相关的所有东西都抽离出去
mutation
负责更新state
,mutation
都是同步操作,commit mutation
下一行state
就更新完了
预先注册在store
中,每次commit
时查mutation
表,执行对应的state
更新函数
注意,要求mutation
必须是同步的,否则调试工具拿不到正确的状态快照(如果异步修改状态的话),会破坏状态追踪
action
用来应对异步场景,作为mutation
的补充
Vuex相当于把Flux里的action
按同步异步分为mutation
和action
action
不像mutation
一样直接修改state
,而是通过commit mutation
来间接修改,也就是说只有mutation
对应原子级的状态更新操作
action
里可以有异步操作,设计上故意把异步作为action
和同步的mutation
分开
异步流程控制
异步流程控制可以通过让action
返回promise
来解决,比传入回调函数优雅一些
Vuex v2.x(目前2017/7/1最新v2.3.0)的store.dispatch
默认返回promise
,非promise
的action
返回值会经Promise.resolve()
包装成promise
dispatch(type: string, payload?: any, options?: Object) | dispatch(action: Object, options?: Object)
Dispatch an action. options can have root: true that allows to dispatch root actions in namespaced modules. Returns a Promise that resolves all triggered action handlers.
(摘自API Reference)
但对于异步操作没有意义(Promise.resolve(undefined)
),需要控制异步流程的话,还是应该手动返回promise
,并把需要的信息从内层promise
传递出来
module
模块化机制,用来拆分组织store
提供namespaced
选项,注册时把模块路径作为前缀。很精致的设计,通过向模块注入local.dispatch/commit/getters/state
来抹平命名空间的影响,模块内不用带命名空间,模块外(业务或者其它模块)需要带命名空间。这样命名空间就变成了一个开关选项,对store
部分没有任何影响
四.工具
同样,Vuex也需要处理state -> view
的部分(作用类似于react-redux
,把状态管理层接入视图层)
支持精确数据绑定的Vue不用像React那么麻烦(往虚拟DOM树上插一些container
,把store.state
的变化传播下去),只需要把store.state
和组件状态连接起来就行,像软链接一样,组件与store
共享state
对象,state
的变化通过响应式特性传递给组件,视图得到更新
mapState
把store.state
和组件的computed
连接起来
注意:mapState
能够强制禁止在组件里直接修改computed
影响实际状态(通过mapState
生成的计算属性是只读的)
{
configurable: true,
enumerable: true,
get: function computedGetter(),
set: function noop()
}
mapGetters
把store.getter
和组件的computed
连接起来
与mapState
类似,也有写保护
mapMutations
把mutation
和组件的methods
连接起来
简化组件commit mutation
的过程(需要在顶层注入store
)
mapActions
把action
和组件的methods
连接起来
简化dispatch action
的过程(同样需要注入store
)
五.疑问
1.怎样避免相同组件共享状态?
比如list里有3个相同组件,怎样避免共享state
带来的状态一致问题?
模块复用与状态共享的冲突。像处理data
一样,用函数state
返回新状态对象,而不用对象state
。这样3个组件对应的state
(store.state
上的一小块)都是独立的,而且不需要额外的状态管理
注意,函数state
的特性在Vuex v2.3.0+可用,低版本需要考虑别的方式,比如:
从
state
提升一级(维护一个数组,管理state list
)考虑把无法共享的局部状态放到组件级,把可共享的数据及操作放到
store
里
第一种方式会让store
迅速膨胀,而且action/mutation
等等都需要index
,组件需要把index
传回给store
,太麻烦不可取
第二种方式是终极解决办法,划分state
的技巧在哪里都适用,不要单纯为了Vuex化而使用Vuex。把所有状态都从组件抽离出来放在store
里也不是不可以,但store
持有的状态过于细致的话,对开发维护来说都是巨大的麻烦:
开发时组件里的任何一个细微变化,都要走
dispatch/commit
维护时会面对一个非常复杂的
store
,上千个mutation type
而这麻烦完全是自找的。那么考虑状态该如何划分:
交互相关的UI状态,放在组件级。比如展开/收起、
loading
显示/隐藏、tab
/表格分页等等无法共享的数据状态,放在组件级。比如表单输入数据
可共享的数据状态,放在状态层。比如可缓存的服务数据
store
的角色应该是server + database
,作为前端数据层存在,而不是单纯地把应用状态从组件树抽离出来作为状态树,没有太大意义
2.computed属性和vuex的store.state怎么关联起来的?
运行时依赖收集机制
// 组件
computed: {
user() {
return this.$store.state.user;
}
}
// store
mutations: {
[types.SET_USER] (state, user) {
state.user = user;
}
}
计算各computed
属性,执行user()
过程中访问了store.state.user
,触发state
的getter
,把user()
函数依赖store.state.user
这个信息记录下来
之后commit mutation
修改了store.state.user
的话,触发state
的setter
,对user
属性对应的所有依赖项(其中有user()
函数)重新求值
接着触发computed
的setter
,执行computed.user
对应的所有依赖项(其中有视图更新函数),视图更新完成
P.S.依赖收集机制的具体实现见vue/src/core/observer/dep.js
3.store传递机制
与react-redux的Provider
类似,也提供了一次注入全局可用的方式(Vue.use(Vuex)
并在new
顶层组件时传入store
)
Vuex作为插件,通过修改Vue.prototype
,把$store
挂上去,让所有vm
共享
4.input等双向绑定场景与store.state不能直接修改的冲突
通过计算属性的getter/setter
来处理:
getter
里读store.state
setter
里commit mutation
写store.state