Skip to content

手写简版Vuex

简单使用

javascript
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const store =  new Vuex.Store({
  modules: {
    user,
    app,
    // ...
  },
  getters
})

new Vue({
  el: '#app',
  store,
  components: { App },
  render: (h) => h(App)
})

实现效果

vuex 实现效果预览

实现思路

  1. vuex是通过Vue.use使用的,所以需要实现install方法:

    • install 里需要在Vue全局挂载$store方法,因此可以用Vue.mixin全局混入配置。
  2. 通过$store.state可以访问状态,状态改变时,需要更新视图。所以state需要设置为响应式,这样在视图中使用state的时候会进行依赖收集,绑定state和相应Watcher的关系。之后state改变就会触发Watcher更新。

    • 一种方法是通过实例化Vue,将state作为data进行响应式处理。
    • 一种方式是用Vue.observablestate对象设置为响应式。
  3. 触发更新:

    • commit处理同步,找到mutations里的对应函数,然后执行函数更新state
    • dispatch处理异步,找到actions里的对应函数,然后执行函数更新state
  4. getters具备计算属性,可以通过Vue中的computed来进行处理(可能这也是Vuex用实例化方式处理state响应式的原因)。

  5. modules的实现是根据store配置递归建立相应的module,并且建立module之间的父子关系。再根据namespace来分割各个模块,使得commit/dispatch的时候需要指定模块的namespace

简化版实现代码

javascript
let _Vue

class Store {
  constructor(options = {}) {
    const { state, mutations, actions, getters } = options
    this.mutations = mutations
    this.actions = actions
    this.getters = getters
    
    let computed = {}
    const self = this
    // 通过 computed 来实现 getters
    Object.entries(getters).map(([getterName, getter]) => {
      computed[getterName] = () => {
        // 封装一层,添加 this.state 作为参数
        return getter(this.state)
      }
      // 访问 getters 的 key 就是访问 computed 的 key
      Object.defineProperty(this.getters, getterName, {
        get() {
          return self._vm[getterName]
        }
      })
    })

    this._vm = new _Vue({
      data: {
        $$state: state
      },
      computed: {
        ...computed
      }
    })

    // 同样可以实现响应式,但是 getters 实现又需要单独处理,比较麻烦
    // this.$$state = _Vue.observable(state)
  }

  get state () {
    return this._vm._data.$$state
  }

  set state (val) {
    throw new Error('无法直接修改 state')
  }

  commit(type, payload) {
    const handler = this.mutations[type]
    handler(this.state, payload)
  }

  // 这里没做太多处理
  dispatch(type, payload) {
    const handler = this.actions[type]
    return handler(this.state, payload)
  }
}

const install = (Vue) => {
  // 类似于单例模式
  if (_Vue === Vue) return
  _Vue = Vue
  Vue.mixin({
    beforeCreate () {
      const options = this.$options
      // 注入 $store 变量
      if (options.store) {
        this.$store = options.store
      } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store
      }
    }
  })
}

export default {
  Store,
  install
}

问题

Q:为什么要commit/dispatch两种形式来处理,都使用dispatch不行吗?

  • 一种可能的原因是单一职责,commit主要处理同步任务,触发状态更新;dispatch主要处理异步任务
  • 另一种原因是方便devtools追踪状态变化。参考这里的评论