如何实现一个简单的Vuex

184 阅读4分钟

如何实现一个简单的Vuex

在Vue项目中我们经常会用到Vuex来统一管理我们的组件状态,在使用Vuex的过程中我经常会思考以下几个问题:

  1. Vue.use(Vuex)发生了什么?
  2. 为什么store.state中的数据发生了改变会导致组件重新渲染?
  3. Vuex中的getters是如何实现的?
  4. Vuex中mutatuion是如何实现的?
  5. Vuex的action是如何实现的?

在这边文章中我会通过实现一个简易版Vuex的方式来对以上的问题作出一一回答。

1. Vue.use(Vuex)发生了什么?

在Vue项目中,当我们要使用Vuex时有三个步骤是我们一定要做的。第一是使用Vue.use(Vuex)将Vuex以Vue插件的形式引入到我们的项目中,第二是创建一个Store实例,第三是将Store实例添加到Vue根组件实例的选项对象中。如下代码所示:

import Vue from 'vue'
import Vuex from 'vuex'
import App from './App.vue'

// 步骤一,以Vue插件的形式将Vuex引入到我们的项目中
Vue.use(Vuex)

// 步骤二,创建一个Store实例
cosnt store = new Vuex.Store({
  state: {
    counter: 0,
  },
  mutations: {
    add(state) {
      state.counter++
    }
  },
  actions: {
    add({ commit }) {
      setTimeout(() => {
        commit('add')
      }, 1000);
    }
  },
})

// 步骤三,将Store实例添加到Vue根组件实例的选项对象中
new Vue({
  store,
  render: h => h(App)
})

以上代码是我们使用Vuex时在main.js中的常规写法。那么当我们使用Vue.use(Vuex)时发生了什么呢?**答案就是通过执行Vue.use(Vuex)我们将store对象挂载到了每个组件实例的store属性上**。那么为什么要这么做呢?因为这样我们便可以在每个组件实例中以this.store的形式访问到store对象。

好了,现在我们知道了当我们使用Vue.use(Vuex)时发生了什么,那么Vuex是如何通过Vue.use(Vuex)实现将store对象挂载到每个组件实例的$store属性上呢?答案就是通过Vue.mixin(全局混入)。我们可以通过全局混入beforeCreate钩子函数并在钩子函数中实现类似于如下的代码:

this.$store = store // 这里的this指的是当前组件实例

这样每个组件实例在创建时都会调用该钩子函数,从而实现将store对象挂载到$store属性中。代码实现如下:

let Vue

class Store {}

function install(_Vue) {
  Vue = _Vue
  Vue.mixin({
    beforeCreate() {
      let options = this.$options
      if (options.store) {
        this.$store = options.store
      } else if (options.parent && options.parent.store) {
        this.$store = options.parent.store
      }
    }
  })
}

export default {
  Store,
  install,
}

首先,当我们使用Vue.use方法注册插件时,Vue会调用插件的install方法。因此我们必须实现install方法,并在install方法中实现我们的混入逻辑。当调用Vue.use方法时Vue会将Vue的构造函数作为参数传入插件的install方法中,这样我们可以很方便使用全局混入方法。其次,在我们实现的beforeCreate钩子函数中主要逻辑就在if 和 else if的代码块中。当当前组件实例为根组件实例时进入if代码块中,当组件实例不是根组件实例时,就从该组件实例的父组件实例中取出store对象并赋值给this.$store。

2. 为什么store.state中的数据发生了改变会导致组件重新渲染?

我们知道当我们改变Vue选项对象中data属性中的数据时会导致组件的重新渲染,这是因为在创建实例的过程中选项对象的data属性中的数据被转变成了响应式数据。那么如果我们把store.state中的数据也变为响应式数据会怎样呢?如果这样做的话,那么当我们读取store.state中的数据时我们就会收集到依赖(渲染函数),变更store.state中的数据时就会触发渲染函数,导致组件重新渲染。那么问题二的答案便呼之欲出了,之所以store.state中的数据发生了改变会导致组件重新渲染,是因为store.state中的数据是响应式数据。

那么问题来了如果我们要实现一个Vuex,我们该如何让store.state中的数据变为响应式数据呢?这个答案很简单,我们只需创建一个Vue的实例并把store.state挂载到Vue选项对象的data属性中,这样当创建实例时store.state就会被转变为响应式数据。我们的代码如下:

class Store {
  constructor(options) {
    this._vm = new Vue({
      data: {
        ?state: options.state
      }
    })
  }
  get state() {
    return this._vm.$data.?state
  }
}

function install(_Vue) {
  Vue = _Vue
  Vue.mixin({
    beforeCreate() {
      let options = this.$options
      if (options.store) {
        this.$store = options.store
      } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store
      }
    }
  })
}

export default {Store, install}

我们在Store的构造函数中创建了一个Vue实例,并把options.state挂载到了Vue选项对象的data属性上,从而使得options.state转变成为响应式数据。为了避免对store.state进行更改,我们将store.state定义成了取值函数,这样就只能对store.state上属性进行修改而不能直接修改store.state。其实在Vuex中,state属性是定义在Store.prototype上的属性,并且重写了state属性的get和set方法,如下代码所示:

var prototypeAccessors$1 = { state: { configurable: true } };

prototypeAccessors$1.state.get = function () {
  return this._vm._data.?state
};

prototypeAccessors$1.state.set = function (v) {
  if ((process.env.NODE_ENV !== 'production')) {
    assert(false, "use store.replaceState() to explicit replace store state.");
  }
};
Object.defineProperties( Store.prototype, prototypeAccessors$1 );

其实我们所使用的取值函数其实就是Vuex中定义state属性所使用方式的语法糖而已。

3. Vuex中的getters是如何实现的?

在Vuex中还提供了getter属性,在Vuex的官网中我们可以看到对getter属性的解释。它相当于vue中的计算属性,只有当它所依赖的state中的数据发生变化时才会被重新求值。在问题2中我们通过将store.state挂载到Vue选项对象的data属性上实现了Vuex的state,那么对于与Vue中计算属性很像的getters我们自然会联想到通过Vue的计算属性来实现Vuex中的getters。代码如下:

let Vue

class Store {
  constructor(options) {
    this._vm = new Vue({
      data: {
        ?state: options.state
      },
      computed: this.initGetters(options.getters)
    })
  }
  get state() {
    return this._vm.$data.?state
  }
  get getters() {
    return this._vm
  }
}

Store.prototype.initGetters = function (getters) {
  let computed = {}
  Object.keys(getters).forEach(key => {
    computed[key] = () => getters[key](this.state)
  })
  return computed
}

function install(_Vue) {
  Vue = _Vue
  Vue.mixin({
    beforeCreate() {
      let options = this.$options
      if (options.store) {
        this.$store = options.store
      } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store
      }
    }
  })
}


export default { Store, install};

getters的实现方式很简单,我们将从Store构造函数中传入的getters属性中的函数进行了一次包装,然后将包装后的getters对象赋值给Vue选项对象的compted属性,这样我们便以Vue计算属性的形式实现了Vuex。此外为了实现以this.$store.getters.key的形式访问到getters中的数据,我们在Store类中也定义了一个名为getters存取器。

在Vuex的源码中对于getters的实现和我们的思路是一样,就是将getters转换为Vue中的计算属性,从而实现依赖改变getter重新求值。

通过对问题2和问题3的解答,我们不难发现Vuex中对state和getters的实现都必须依赖于Vue,也就是为什么我们常说Vuex与Vue是相耦合的。

4. Vuex中mutatuion是如何实现的?

Vuex中mutatuion的实现原理还是比较简单的,就是当调用commit函数时,根据commit函数的第一个参数来判断是调用mutatuions中的哪个函数然后传参入参数并调用即可。

let Vue

class Store {
  constructor(options) {
    this._vm = new Vue({
      data: {
        ?state: options.state
      },
      computed: this.initGetters(options.getters)
    })
    this._mutations = options.mutations
  }
  get state() {
    return this._vm.$data.?state
  }
  get getters() {
    return this._vm
  }
  commit = (type, payload) => {
    this._mutations[type](this.state, payload)
  }
}

Store.prototype.initGetters = function (getters) {
  let computed = {}
  Object.keys(getters).forEach(key => {
    computed[key] = () => getters[key](this.state)
  })
  return computed
}

function install(_Vue) {
  Vue = _Vue
  Vue.mixin({
    beforeCreate() {
      let options = this.$options
      if (options.store) {
        this.$store = options.store
      } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store

      }
    }
  })
}


export default { Store, install};

对于mutatuion的现实本质上就是如何实现一个commit函数。commit函数实现原理很简单,如上代码所示,就是根据type选择对应mutatuion函数,传入相关的参数并调用即可。

5. Vuex的action是如何实现的?

Vuex支持Action,Action与Mutation作用相同都是用来变更store.state中的数据。但是Action可以包含异步操作,当异步操作执行完成后再以Mutation的方式完成store.state中的数据变更。我们的代码实现如下:

let Vue

class Store {
  constructor(options) {
    this._vm = new Vue({
      data: {
        ?state: options.state
      },
      computed: this.initGetters(options.getters)
    })
    this._mutations = options.mutations
    this._actions = options.actions
  }
  get state() {
    return this._vm.$data.?state
  }
  get getters() {
    return this._vm
  }
  commit = (type, payload) => {
    this._mutations[type](this.state, payload)
  }
  dispatch = (type, payload) => {
    let context = {
      commit: this.commit,
      state: this.state,
    }
    this._actions[type](context, payload)
  }
}

Store.prototype.initGetters = function (getters) {
  let computed = {}
  Object.keys(getters).forEach(key => {
    computed[key] = () => getters[key](this.state)
  })
  return computed
}

function install(_Vue) {
  Vue = _Vue
  Vue.mixin({
    beforeCreate() {
      let options = this.$options
      if (options.store) {
        this.$store = options.store
      } else if (options.parent && options.parent.$store) {
        this.$store = options.parent.$store

      }
    }
  })
}


export default { Store, install};

Vuex中action的实现本质上就是对dispatch方法的实现,dispatch与commit原理差不多,最本质的区别是dispatch在调用对应的action函数时会将scommit方法作为参数传入到action函数中去,这样便可以让action方法做完异步操作后再调用commit方法实现对store.state中数据的修改。

后记

这篇文章通过对5个问题的解答实现了一个简易的Vuex插件。这个Vuex插件非常简单甚至可以说是捡漏,其中有些方法可能少传了一些参数。但这并不妨碍我们对Vuex原理的理解。当然在我们的实现中我并没有将Vuex的modules功能给实现出来,这个我准备留在以后实现,敬请期待!