blog
blog copied to clipboard
放飞自我的 Vuex
改变 Vuex 的原因
在开始之前,我表明一下自己的观点:vuex 很优秀,我很认同 vuex 的设计理念,我只是想实现一个用起来更简单的 vuex
只要用过Vue的前端,基本没有人没接触过 Vuex,vuex的 api 简洁明了,功能简单但强大,上手也是很快的。
vuex 的数据流大致是这样的
state -> vue component -> action -> mutation -> state
| ^
|_________or___________|
这个数据流很清晰,而且职责分的很明确,如果完全按照这个官方标准去编码,项目维护性可变得很高。
mutation
但是,不知道有没有开发者感觉到,编写 action 和 mutation 有些繁琐,因为本来可以一步到位的工作,就因为mutation的限制,不合适做异步、调用action等等原因,不得不分成两步去实现。
mobx 示例
import { observable, computed, action } from 'mobx';
class Counter {
@observable num = 0
@computed get numPlus() {
return this.num + 1
}
@action plus() {
this.num++
}
@action reset() {
this.num = 0
}
@action async delayPlus() {
return new Promise(resolve => {
setTimeout(() => {
runInAction(() => {
this.num++
})
resolve()
}, 500)
})
}
}
在使用 mobx 的时候,我被它那种简洁写法吸引了,没有mutation,只是在需要更改状态的函数用 mobx 的工具函数包装一下,很好的控制了状态变更的范围。在代码简洁方面,比 vuex 做的更好。
习惯了mobx这种写法后,在我看来, vuex 的 mutation 根本就是多余的 。
在 vuex 的文档中,提到了严格模式, 如果状态变更不是有mutation函数引起的,将会抛出错误。
在 action 中其实也是可以直接修改状态的,并且修改后依然是响应式的,对应视图依然会更新,只是 vuex 强烈不建议这么做,不然也不会有严格模式出现。
dispatch & commit
mutation 和 action 的调用,需要使用 dispatch 调用action, 使用commit 调用 mutation,而且参数只能有一个,这个其实能理解,vuex 推崇提交一个更新的 payload,而不是多个payload。这个理念是不错的,但是用起来有时候还是不方便。
watch
在 vue 组件中有 watch,能监听到状态变化后自动触发,这个在 vue 组件这么普遍的功能,在 vuex 居然没有,所以要监听 vuex 状态变化,只能通过在组件作为载体去做 watch 功能。
放飞自我
上面写了使用 vuex 的一些不太舒服的方面点。我该着重解决他们了
改造前思考
vuex 已经被大众所接受,如果我的更改会导致原本 vuex api 不兼容,那很难在原本项目中迁移过去,所以vuex原本的 api 不能动,而是要在原本 api 上扩展。
所以我的做法就是,原本注入到 Vue 组件中的 $store
变量不动,没有对它有任何的侵入或者 hack,而是利用 $store 现有的api,重新构建一个类似 $store
的变量,我取名为 $s
。而这次改造我大量使用 Object.defineProperties
去代理原本的 state、action、mutation 等等, 所以我给这个项目命名为 vuex-proxy
改造结果
vuex-proxy
就是我基于以上的所述的优化,我直接引用了原本的readme过来
Vuex Proxy
that mean Vuex Proxy
vue 的增强组件,基于 vuex,让 vuex 更简单
使用方法
首先,vuex 那整套完全兼容,所以可以从 vuex 无缝迁移到 vuex-proxy,但是反之不行,因为 vuex-proxy 在 vuex 的api上有扩展
API
在 vue 组件实例中,增加了一个
$s
属性,这是vuex-proxy store
,事实上这是一个vuex store
的代理,目的就是为了简化vuex
使用,当然,原本 vuex 注入的$store
依然有效注意在定义 store 的 state,actions,getters,mutation 时,不要和这些 api 名字重复了
vuex-proxy store 格式
在组件内使用
this.$s
访问store 定义
store 的定义和vuex完全兼容,
{ // 完全兼容 vuex 的 store 定义 namespaced: true, state: { list: [], total: 0, }, modules: {}, // 但是 actions 和 mutations 的作用变得平等,没有区别,并且this指向当前 vuex proxy store,详见下文 actions: {}, mutations: {}, // 新增 api,在state发生变化的时候,触发函数 watch: { list(newValue, oldValue) { console.log('list change:', oldValue, ' => ', newValue) } }, }
this.$s.$store
原始的 vuex store,没有任何侵入和 hack
this.$s.$rootVM
挂载 store 最顶层的组件实例
this.$s.$root
最顶层 vuex-proxy store 对象
this.$s.$registerModule
动态注册新模块,参数和功能与 vuex 的 registerModule 基本一致
this.$s.$unregisterModule
动态删除模块,参数和功能与 vuex 的 unregisterModule 基本一致
this.$s.$state
该模块级别的 vuex store 状态数据
this.$s[moduleName]
模块级别的 vuex-proxy store,api 和根 vuex-proxy store 无区别,只是状态数据不一样
this.$s[fieldName]
fieldName 是指 state,getters,actions,mutation 里面的所有字段名,vuex-p 把所有的状态、计算属性、方法都放到了同一层级里面,当你访问 vuex-proxy 的数据时,内部是知道你访问的是 state,还是getters,,还是 actions ,是一个 module,所以这也要求 state,getters,actions,mutation 里面的字段不能有重复,如果有重复则在初始化的时候会报错误
示例
import Vue from 'vue' import vuexProxy from 'vuex-proxy' // 使用插件 Vue.use(vuexProxy) new Vue({ // 在根组件使用 store 属性定义 vuex-proxy store,vuexp store 的 api 和 vuex store 完全兼容,说明请看下文 store: { // store state 状态数据,和 vuex state 完全一致,无任何变化 state: { num: 0, }, // store getters 计算属性,和 vuex state 完全一致,无任何变化 getters: { numPlus: state => state + 1 }, // watch 与 vue 的 watch 相似,当 state 变化后触发,支持 state 和 getters 的监听 watch: { num: 'consoleNum', // 值可以是字符串,表示 action 或 mutation 的函数名 numPlus(newV, oldV) { // 值可以是函数 console.log('num change:', oldV, ' => ', newV) }, }, actions: { // 第一种 action 写法,和 vuex state 完全一致,无任何变化,在组件调用的时候,也没有区别,使用 this.$store.dispatch('reset') // 注意:该写法 // this 指向 vuex store // 第一个参数是 vuex 的固定格式 { dispatch, commit, getters, state, rootGetters, rootState } // 第二个参数是 action 参数 // 只有两个参数,不支持更多参数 reset({commit}) { commit('RESET_NUM') }, // 第二种 action 写法,增强版本,在组件调用的时候,使用 this.$s.plus() // this 指向 vuex-proxy store // 参数无限个数,可在里面直接更改 state,把它当做 vuex mutation 来用,支持异步,注意异步函数里的 this 是指向的 vuex-proxy store就没问题了 plus() { return ++this.num }, setNum(n) { // 在action 函数内部,可以访问 state console.log(this.num) // 也可以访问 getters 计算属性 console.log(this.numPlus) // 也可以调用其他 action 和 mutation this.plus() // 也可以修改 state this.num = n }, consoleNum() { console.log(this.num) } }, mutations: { // 第一种 mutations 写法,和 vuex state 完全一致 RESET_NUM(state) { state.num = 0 } // 第二种 mutations 写法,和第二种 action 写法没有区别,用法也没有区别 resetNum() { this.num = 0 } }, // 嵌套模块,支持无限嵌套 modules: { testMod: { state: { test: 100 }, getters: {}, actions: {}, } } }, data() { return { name: 'my name is vue plus' } } // 映射到计算属性中,用 $computed,完全兼容原本 vue 组件的 computed 功能 // 使用字符串数组形式,直接写key,多层级直接使用 . 或者 / 分隔,最终映射的key名字是最后一层的key,并且自动绑定了 get 和 set,也就是可以直接给绑定的对象赋值 $computed: ['num', 'numPlus', 'testMod.test'], mounted() { this.num = 2 // 相当于 this.$s.num = 2 this.test = 20 // 相当于 this.$s.testMod.test = 20 }, // 使用对象形式 // this 指向组件实例 $computed: { num: 'num', // 会自动绑定 get 和 set xnum: { get($s) { return $s.num } // get 函数只有一个参数,该参数为 vuex-proxy store 实例,也就是 this.$s set(n, $s) { return $s.num = n } // set 函数有两个参数,第一个是修改后的值,第二个是 this.$s }, numPlus() { // 这算是 get 函数 return this.$s.num }, myname() { // 还可以访问组件内部 data return this.name }, }, // 绑定 actions 和 mutations 到组件实例中 // 字符串数组形式,根据key名字自动映射,映射后函数的this指向为函数所在的层级的 vuex-proxy store 实例 $methods: ['plus', 'setNum'], $methods: { plus: 'plus', setNum(n) { return this.$s.setNum(n) }, sayMyName() { console.log(this.myname) } }, watch: { // 这样监听值改变,api 无变化 '$s.num': function(oldv, newv) { console.log(this.newv) } } })
对比
与 Vuex 对比,api 变化
目的:不破坏 vuex 前提下,让 vuex 变得更简单,更强大
兼容性
Vuex 的原有功能一切正常,可以无缝的将 vuex 迁移到 vuex-proxy
为什么要改变 vuex
vuex 是 vue 官方指定并维护的状态管理插件,和 vue 的结合无疑非常好的,但是在我看来在使用vuex的时候,有一些让我不舒服的地方
- actions 和 mutations 的参数,只能有一个,我理解初衷其实是为了只有一个 payload,更好记录,调试,跟踪变更等等,但是却不好用
- mutations 的存在,我觉得就是多余的,明明可以直接改状态,为什么还要多包装一层呢。我觉得有几个原因: 2.1 方便调试工具的 Time Revel,redo,undo,变化跟踪等等。但是相信我,这些功能你基本不会用得上的,调试工具最大的作用就是用来看当前状态数据。 2.2 隔离 actions 的副作用,让状态变更更好跟踪和调试。但是实际上用的时候,我基本上不会去调试 mutation 函数
- 在组件调用的时候,必须要用 dispatch 或 commit 去调用,为什么呢,直接调用不是更好吗
变化点
- 初始化时,new vues.Store 是可选的,可以 new vuex.Store 再传入,也可以直接传入,内部自动识别
- actions 和 mutations 兼容原有的,并且支持不同写法
- state,getters,modules 没有变更
- vue开发工具依然可用,不过每次更改状态都会有一个名为
VUEXP_CHANGE_STATE
的 type- vuex 生态的插件都可以继续使用
- 新增 watch api,监听 state 和 getters 变化,和 vue 组件的 watch 功能类似
简单地说,就是原有的 vuex 的功能都没有阉割,没有改变,只是增加了其他用法,使其变得使用更简单
还是mobx好使,vuex一开始接触到看到是一整个树上,还是感觉挺好的。但是使用起来发现是没有mobx好用的,平常用的都是mobx-state-tree,真的很方便。但是vuex,和你有同感,就是感觉变得繁琐了。
@SouWinds mobx 集成 vue 有点难搞,我曾经尝试过,可以看看这个项目,后面有些bug实在修不好,就改变思路对 vuex 下手了