Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

放飞自我的 Vuex #28

Open
eyasliu opened this issue May 7, 2020 · 2 comments
Open

放飞自我的 Vuex #28

eyasliu opened this issue May 7, 2020 · 2 comments

Comments

@eyasliu
Copy link
Owner

eyasliu commented May 7, 2020

改变 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 的功能都没有阉割,没有改变,只是增加了其他用法,使其变得使用更简单

@n1203
Copy link

n1203 commented Aug 18, 2020

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

@eyasliu
Copy link
Owner Author

eyasliu commented Aug 19, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants