Vuex

一.出发点

在相对独立的组件中,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细分成了actionmutation,分别应对异步场景和同步场景,由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里,要么放在模版里,提供getterstate相关的所有东西都抽离出去

mutation

负责更新statemutation都是同步操作commit mutation下一行state就更新完了

预先注册在store中,每次commit时查mutation表,执行对应的state更新函数

注意,要求mutation必须是同步的,否则调试工具拿不到正确的状态快照(如果异步修改状态的话),会破坏状态追踪

action

用来应对异步场景,作为mutation的补充

Vuex相当于把Flux里的action按同步异步分为mutationaction

action不像mutation一样直接修改state,而是通过commit mutation来间接修改,也就是说只有mutation对应原子级的状态更新操作

action里可以有异步操作,设计上故意把异步作为action和同步的mutation分开

异步流程控制

异步流程控制可以通过让action返回promise来解决,比传入回调函数优雅一些

Vuex v2.x(目前2017/7/1最新v2.3.0)的store.dispatch默认返回promise,非promiseaction返回值会经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个组件对应的statestore.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,触发stategetter,把user()函数依赖store.state.user这个信息记录下来

之后commit mutation修改了store.state.user的话,触发statesetter,对user属性对应的所有依赖项(其中有user()函数)重新求值

接着触发computedsetter,执行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

  • settercommit mutationstore.state

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code