blog icon indicating copy to clipboard operation
blog copied to clipboard

放飞自我的 Vuex

Open eyasliu opened this issue 4 years ago • 2 comments

改变 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的时候,有一些让我不舒服的地方

  1. actions 和 mutations 的参数,只能有一个,我理解初衷其实是为了只有一个 payload,更好记录,调试,跟踪变更等等,但是却不好用
  2. mutations 的存在,我觉得就是多余的,明明可以直接改状态,为什么还要多包装一层呢。我觉得有几个原因: 2.1 方便调试工具的 Time Revel,redo,undo,变化跟踪等等。但是相信我,这些功能你基本不会用得上的,调试工具最大的作用就是用来看当前状态数据。 2.2 隔离 actions 的副作用,让状态变更更好跟踪和调试。但是实际上用的时候,我基本上不会去调试 mutation 函数
  3. 在组件调用的时候,必须要用 dispatch 或 commit 去调用,为什么呢,直接调用不是更好吗

变化点

  1. 初始化时,new vues.Store 是可选的,可以 new vuex.Store 再传入,也可以直接传入,内部自动识别
  2. actions 和 mutations 兼容原有的,并且支持不同写法
  3. state,getters,modules 没有变更
  4. vue开发工具依然可用,不过每次更改状态都会有一个名为 VUEXP_CHANGE_STATE 的 type
  5. vuex 生态的插件都可以继续使用
  6. 新增 watch api,监听 state 和 getters 变化,和 vue 组件的 watch 功能类似

简单地说,就是原有的 vuex 的功能都没有阉割,没有改变,只是增加了其他用法,使其变得使用更简单

eyasliu avatar May 07 '20 07:05 eyasliu

还是mobx好使,vuex一开始接触到看到是一整个树上,还是感觉挺好的。但是使用起来发现是没有mobx好用的,平常用的都是mobx-state-tree,真的很方便。但是vuex,和你有同感,就是感觉变得繁琐了。

n1203 avatar Aug 18 '20 03:08 n1203

@SouWinds mobx 集成 vue 有点难搞,我曾经尝试过,可以看看这个项目,后面有些bug实在修不好,就改变思路对 vuex 下手了

eyasliu avatar Aug 19 '20 11:08 eyasliu