手写简易版Vuex,初探Vuex原理

831 阅读3分钟

一、Vuex是如何使用的

Vue项目中,使用vuex的步骤大概如下:

  1. src下创建一个store/index.js,然后添加如下代码
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {},
  getters: {},
  mutations: {},
  actions: {}
})
  1. main.js引入
import Vue from 'vue'
import App from './App.vue'
import store from './store/index'

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

然后就可以通过this.$store访问了。

二、开始手写

1、创建文件

src/store下新建一个自己手写的文件vuex.js。通过前面官方vuex的使用可以知道,vuex主要用到了两个东西:Vue.use()new Vuex.Store()

  • Vue.useAPI 要求传入的参数是一个对象或函数,如果是对象的话对象里需要包含一个install方法。

  • new Vuex.Store({})则需要我们创建一个Store类。

所以我们首先需要创建如下最基本的满足条件:一个Store类和一个install方法。

// src/store/vuex.js
class Store {
    constructor() {}
}

const install = (Vue) => {}

const vuex = {
  Store,
  install
}

export default vuex

然后替换掉之前引入的官方vuex,改成我们自己实现的vuex

// src/store/index.js
import Vue from 'vue'
// import Vuex from 'vuex'
import Vuex from './vuex.js'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    name: '张三'
  },
  getters: {},
  mutations: {},
  actions: {}
})

替换后发现一切正常,没什么报错信息出现,接下来我们一步一步来实现功能。

2、实现install

前面在main.js里我们仅仅是在根组件注入了store,我们希望所有组件都能拥有store,所以我们可以使用Vue.mixin进行全局混入。

const install = (Vue) => {
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) { // 根组件
        this.$store = this.$options.store
      } else if (this.$parent && this.$parent.$store) { // 子组件
        this.$store = this.$parent.$store
      }
    }
  })
}

$options里可以获取我们一开始在根组件里注入的store,然后赋值给$store。 由于父组件的beforeCreate执行先于子组件的beforeCreate,所以我们在beforeCreate里可以拿到父组件已经存在的$store并将其赋值给子组件,使得每个组件都拥有$store,都可以通过this.$store调用。

3、实现state

现在我们通过this.$store.state是无法取到值的,前面我们通过new传入了一个对象:

{
  state: {
    name: '张三'
  },
  getters: {},
  mutations: {},
  actions: {}
}

所以我们可以在类的构造器里获得这个对象参数。

class Store {
  constructor(options) {
    this.state = options.state || {}
  }
}

打印this.$store.state

image.png

到这里我们的工作还没完成,因为state是支持响应式的,如何让state实现响应式?我们都知道Vue里的data是响应式的,所有我们可以借助它来实现我们想要的效果。

class Store {
  constructor(options) {
    // this.state = options.state || {}
    this._vm = new Vue({
      data: {
        state: options.state || {}
      }
    })
  }

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

state传入new Vue()里的data,这样我们便很容易的将state变成了一个响应式数据,此时我们便可以通过this.$store._vm.state获取数据了,但为了方便,后面我们增加get接口并返回_vm.state,方便使用时可以通过this.$store.state取值。

接下来简单测试一下是否生效:

<div>{{ $store.state.name }}</div>
created() {
    setTimeout(() => {
      // 实际使用中不提倡这种直接修改state的状态
      this.$store.state.name = '李四'
    }, 2000)
},

2秒后可以看到页面视图数据从张三变成了李四,说明我们的state响应式是OK的。

4、实现getters

先看看我们是如何使用getters的:

{
  state: {
    name: '张三'
  },
  getters: {
    getHello: (state) => {
      return `hello,${state.name}`
    }
  },
  mutations: {},
  actions: {}
}

可以看到getters对象里实际是一些函数,函数参数里可以拿到state,所以我们在实现时需要把state作为参数抛出来。

class Store {
  constructor(options) {
    // this.state = options.state || {}
    this._vm = new Vue({
      data: {
        state: options.state || {}
      }
    })
    // 新增代码
    const getters = options.getters || {}
    this.getters = {}
    Object.keys(getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => {
          return getters[key](this.state)
        }
      })
    })
  }

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

因为我们在使用gettters时是不需要写括号的:this.$store.getters.getHello,所以采用Object.definePropertyget接口实现。

5、实现mutations

mutations是通过commit触发的,因此我们在将传入的mutations对象存储起来后还需要实现commit方法,然后commit根据第一个参数的值触发mutations里对应的方法。

this.$store.commit('changeName', payload)
mutations: {
  changeName(state, payload) {
    state.name = payload
  }
}

新增mutations实现代码:

class Store {
  constructor(options) {
    // 省略前面的代码....

    const mutations = options.mutations || {}
    this.mutations = {}
    Object.keys(mutations).forEach(key => {
      // 将函数存储进this.mutations里
      this.mutations[key] = (param) => {
        mutations[key](this.state, param)
      }
    })
  }

  commit = (type, param) => {
    // 执行this.mutations里对应函数
    this.mutations[type](param)
  }

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

6、实现actions

actionsmutations类似,不同的是actions是通过dispatch调用,在actions里的方法的第一个参数是Store

mutations: {
  changeName(state, payload) {
    state.name = payload
  }
},
actions: {
  changeNameAction({ commit }, payload) {
    commit('changeName', payload)
  }
}
setTimeout(() => {
  this.$store.dispatch('changeNameAction', '李四')
}, 3000)

新增actions实现代码:

class Store {
  constructor(options) {
    // 省略前面的代码....

    const actions = options.actions || {}
    this.actions = {}
    Object.keys(actions).forEach(key => {
      this.actions[key] = (param) => {
        actions[key](this, param)
      }
    })
  }

  dispatch = (type, param) => {
    this.actions[type](param)
  }

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

三、完整代码

import Vue from 'vue'

class Store {
  constructor(options) {
    this._vm = new Vue({
      data: {
        state: options.state || {}
      }
    })
    
    const getters = options.getters || {}
    this.getters = {}
    Object.keys(getters).forEach(key => {
      Object.defineProperty(this.getters, key, {
        get: () => {
          return getters[key](this.state)
        }
      })
    })

    const mutations = options.mutations || {}
    this.mutations = {}
    Object.keys(mutations).forEach(key => {
      this.mutations[key] = (param) => {
        // 将函数存储进this.mutations里
        mutations[key](this.state, param)
      }
    })
    
    const actions = options.actions || {}
    this.actions = {}
    Object.keys(actions).forEach(key => {
      this.actions[key] = (param) => {
        actions[key](this, param)
      }
    })
  }

  dispatch = (type, param) => {
    this.actions[type](param)
  }

  commit = (type, param) => {
    // 执行this.mutations里对应函数
    this.mutations[type](param)
  }

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

const install = (Vue) => {
  Vue.mixin({
    beforeCreate() {
      if (this.$options.store) { // 根组件
        this.$store = this.$options.store
      } else if (this.$parent && this.$parent.$store) { // 子组件
        this.$store = this.$parent.$store
      }
    }
  })
}

const vuex = {
  Store,
  install
}

export default vuex