目录
- install 方法
- Store 类
- State
- Mutations
- Actions
- 小结一下
- Modules
闲话不多说,让我们开始实现一个简单的 Vuex
首先,回想一下 Vuex 的使用方法,这里给出一个简单的使用例子:
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    a: 1
  },
  getters: {
    aPlus(state){
      return state.a + 1
    }
  },
  mutations: {
    addA(state, payload){
      state.a += payload
    }
  },
  actions: {
    asyncAddA({commit, state}, payload){
      setTimeout(() => {
        commit('addA', payload)
      }, 1000)
    }
  },
  modules: {
    a: {
      modules: {
        c: {
          state: {
            c1: 'c1'
          }
        }
      } 
    },
    b: {
      state: {
        b: 1
      },
      mutations: {
        bPlus(state, payload){}
      },
      getters: {
        bPlus(state){}
      }
    }
  }
})
// 根组件实例
new Vue({
  store
})
可以看到,'vuex' 导出来的应该是一个对象,上面有至少两个属性:install 和 Store,install 方法在 Vue.use(Vuex) 时会执行,Store 是一个类,我们先把大体的架子写出来:
let Vue  // 后面要用到
class Store{
  constructor(options){
    // ...
  }
}
function install(){}
export {
  install,
  Store
}
1. install 方法
接下来,我们看一下 install 方法的实现:
用过 Vuex 的都知道:当在根组件注入 store 后,每个子组件都能访问到这个 store,这其实就是 install 帮我们做到的,先来看看这个方法的实现:
let Vue
function install(_Vue){
  // 重复安装 Vuex
  if(Vue) throw new Error('Vuex instance can only exist one!')
  
  Vue = _Vue
  
  // 将 vm.$options.$store 注入到每个子组件的 vm.$store 中
  Vue.mixin({
    beforeCreate() {
      if(this.$options && this.$options.store){
        this.$store = this.$options.store
      } else {
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}
Vue.use(Vuex) 会将 Vue 传入 install 方法,这个全局变量可以用来判断是否已经被 use 过了,重复则报错。
如果没有重复,使用 Vue.mixin 混入一段代码,这段代码会在 beforeCreate 这个钩子函数执行之前执行
核心代码是这几行:
if(this.$options && this.$options.store){
  this.$store = this.$options.store
} else {
  this.$store = this.$parent && this.$parent.$store
}
this 指向当前组件实例,如果当前组件实例的 $options 上有 store 属性,说明该实例是注入 store 的根实例,直接往 $store 上挂载 store
反之,else 中会去检查当前实例的父组件: $parent 上有没有 $store,有则在实例上挂载该属性
因为组件的生命周期顺序是:父组件先创建,然后子组件再创建,也就是说子组件执行 beforeCreate 钩子函数时,父组件的 store 已经注入了,所以可以实现循环注入。
2. Store 类
接下来,我们实现一下 Store 类:
Store 中有四部分需要我们实现:State、Getter、Mutation、Action,我们先来实现 State:
(1) State
通过 $store.state.xxx 即可访问到 vuex 中的 state 状态
class Store{
  constructor(options){
    this._state = new Vue({ data:options.state })
  }
  get state(){ 
    return this._state
  }
}
这里我们不直接通过 store.state 提供访问,而是通过访问器形式提供访问,可以避免 state 被外部直接修改。
另外要注意的一个点是:我们借用 Vue 让 state 变成响应式数据了。这样的话,当 state 变化的时候,依赖的地方都能得到更新通知
以上,我们简单实现了 State 状态
(2) Getters
接下来实现 Getters,Getters 是通过 store.getters.x 的形式访问的
首先,将 options 中的 getters 遍历,将每个属性逐个挂载到 store.getters 上。
因为 getters 的特点是:访问属性,返回函数执行,很容易想到可以用访问器实现。
constructor(options){
// ...
this.getters = {}
Object.keys(getters).forEach(k => {
  Object.defineProperty(this._getters, k, {
    get: ()=>{  // 箭头函数保证 this 能够访问到当前 store 实例
      return getters[k](this.state)
    }
  })
})
}
(3) Mutations
mutations 通过 store.commit(type, payload) 触发,commit 内部会通过 type 取到 this._mutations 上对应的 mutation,将 payload 传入并执行
我们需要将 options 上的 mutations 上进行遍历,定义到 this._mutations 上,之所以这样重新定义一遍是为了能够在 mutation 函数外面封装一层,方便传入 state
constructor(options){
  // ...
  this._mutations = {}
  Object.keys(mutations).forEach(k => {
      this._mutations[k] = (payload) => {
      mutations[k](this.state, payload) // 注意 state 参数的传入
    }
  })
}
commit = (type, payload) => { // 箭头函数,保证 this 指向 store
  return this._mutations[type](payload)
}
(4) Actions
actions 的实现和 mutations 相似,就不再赘述了。
不同点在于:
(1) 执行回调传入的参数不同
(2) dispatch 返回的应该是一个 Promise
constructor(options){
// ...
  this._actions = {}
  Object.keys(actions).forEach(k => {
    this._actions[k] = (payload) => {
      actions[k](this, payload) // 这里直接将整个 store 传入了
    }
  })
}
dispatch = (type, payload) => {
  return new Promise((resolve, reject) => {
    try{
      resolve(this._actions[type](payload))
    } catch (e){
      reject(e)
    }
  })
}
(5) 小结一下
OK,现在让我们把上面的这些代码片段写到一块
let Vue
class Store{
  constructor(options){
    this._state = new Vue({
      data: { state: options.state }
    })
    // 生成 Getters、Mutations、Actions
    this.generateGetters(options.getters)
    this.generateMutations(options.mutations)
    this.generateActions(options.actions)
  }
  generateGetters(getters = {}){
    this.getters = {}
    Object.keys(getters).forEach(k => {
      Object.defineProperty(this.getters, k, {
        get: ()=>{
          return getters[k](this.state)
        }
      })
    })
  }
  generateMutations(mutations = {}){
    this._mutations = {}
    Object.keys(mutations).forEach(k => {
      this._mutations[k] = (payload) => {
        mutations[k](this.state, payload)
      }
    })
  }
  generateActions(actions = {}){
    this._actions = {}
    Object.keys(actions).forEach(k => {
      this._actions[k] = (payload) => {
        actions[k](this, payload)
      }
    })
  }
  commit = (type, payload) => {
    return this._mutations[type](payload)
  }
  dispatch = (type, payload) => {
    return new Promise((resolve, reject) => {
      try{
        resolve(this._actions[type](payload))
      } catch (e){
        reject(e)
      }
    })
  }
  get state(){
    return this._state.state
  }
}
function install(_Vue){
  if(Vue) throw new Error('Vuex instance can only exist one!')
  Vue = _Vue
  Vue.mixin({
    beforeCreate() {
      if(this.$options && this.$options.store){
        this.$store = this.$options.store
      } else {
        this.$store = this.$parent && this.$parent.$store
      }
    }
  })
}
export default {
  install,
  Store
}
(6) Modules
接下来,我们要实现 Modules。准备好,前面的代码要发生变动了
我们先贴一下前面的使用例子:
new Vuex.Store({
  // ...
  modules: {
    a: {
      modules: {
        c: {
          state: {
            c1: 'c1'
          }
        }
      } 
    },
    b: {
      state: {
        b: 1
      },
      mutations: {
        bPlus(state, payload){}
      },
      getters: {
        bPlus(state){}
      }
    }
  }
})
先总结一下各个属性的访问方式:
对于 c 模块中的状态 c1,访问方式为:store.state.a.c.c1
对于模块的 getters、mutations、actions,都被定义到 store 上了,通过形如 store.getters.xxx 这种方式访问
好了,现在目标明确了,接下来就把上面提到的两个点实现:
首先,我们需要将 store 上 getters、mutations、actions 进行初始化成空对象
我们不再需要用 generateGetters 等方法对 getters、mutations、actions 这些属性进行处理了,这部分代码可以删掉了。在 installModule 的过程中我们会对这些属性进行统一处理。
不过不用担心,我们前面讲到的各个属性的处理方式的核心代码,后面依旧用得上!
constructor(options){
  this._state = new Vue({
    data: { state: options.state }
  })
  // init
  this.getters = {}
  this.mutations = {}
  this.actions = {}
  // this.generateGetters(options.getters)
  // this.generateMutations(options.mutations)
  // this.generateActions(options.actions)
  let modules = options  // 这里取到的 options,实际上就是根模块
  installModule(this, this.state, [], modules) // 对各个子模块进行处理的函数,重点关注!
}
重点关注一下 installModule 方法,在这个方法里我们实现了 state、getters 等属性的收集处理
先解释一下传给这个函数的四个参数:
- 
store实例
- 
store上的state
- 
path:在installModule中需要递归来安装处理子模块,所以用path数组来表示模块的层级关系。数组的最后一位为当前要处理的模块的模块名,最后一位前面的都是当前模块的祖先模块。举个栗子:如果是根模块,传入的path为[],如果传入[a, c],说明当前处理模块名为c,模块层级为:根模块 > a模块 > c模块
- 
module是当前要处理的模块
我们先来把这个函数的基本架子写好,因为模块的嵌套层数是未知的,所以必须用递归进行模块处理安装。
function installModule(store, state, path, module) {
  // 如果当前模块还有 modules(即还有子模块),递归进行模块安装
  if(module.modules){
    // 遍历当前模块的子模块
    eachObj(module.modules, (modName, mod)=>{
      // 将传入的 path 拼接当前要处理的的模块名,得到模块层级数组并传入,进行子模块安装
      installModule(store, state, path.concat(modName), mod)
    })
  }
}
注意这里的 path.concat(modName) 是为了将祖先模块名拼接到 path 中,这个模块层级数组在后面需要用到,我们后面会讲
另外,这里把遍历对象的方法封装到了 eachObj 中,让代码看起来简洁一点:
function eachObj(obj, callback){
  Object.keys(obj).forEach(k => {
    callback(k, obj[k])
  })
}
接下来我们把模块的 getters、mutations、actions 挂载到根模块的相应属性上,这三者的处理方式大同小异,要注意的点就是:
同名 mutations、actions 不会被覆盖,他们会被依次执行。所以 store.mutations.xxx 和 store.actions.xxx 应该是一个数组,但 getters 不允许同名,直接挂载到 store.getters 上即可
function installModule(store, state, path, module) {
  let getters = module.getters || {}
  // 将模块的 getters 定义到 store.getters 上
  eachObj(getters, (k, fn) => {
    Object.defineProperty(store.getters, k, {
      get(){
        return fn(module.state) // 注意这里传入的 state 是当前模块的局部 state
      }
    })
  })
  let mutations = module.mutations || {}
  eachObj(mutations, (k, fn) => {
    const rootMut = store.mutations // 根模块的 mutations
    // 先检查 rootMut[k] 是否被初始化了,没有的话初始化为空数组
    if(!rootMut[k]) {
      rootMut[k] = []
    }
    rootMut[k].push((payload)=>fn(module.state, payload))
  })
  // actions 类似 mutations 的实现
  let actions = module.actions || {}
  eachObj(actions, (k, fn) => {
    const rootAct = store.actions
    if(!rootAct[k]){
      rootAct[k] = []
    }
    rootAct[k].push((payload)=>fn(store, payload))
  })
  if(module.modules){ // 递归处理模块
    eachObj(module.modules, (modName, mod)=>{
      installModule(store, state, path.concat(modName), mod)
    })
  }
}
接下来我们要实现 state 的挂载,这部分代码相对上面难理解一点:
function installModule(store, state, path, module) {
  let parent = 
    path.slice(0, -1).reduce((state, cur) => {
      return state[cur]
    }, state)
  Vue.set(parent, path[path.length - 1], module.state)
  // 省略处理 getters、mutations、actions 的代码
  // 省略递归处理模块的代码
}
为了保证后面的思路不会乱掉,这里还是要再强调一下 path 的含义:
path 数组来表示模块的层级关系,如果是根模块,传入的 path 为 [],如果传入 [a, c],说明当前处理模块名为 c,模块层级为:根模块 > a模块 > c模块
接下来剖析代码:
path.slice(0, -1) 返回去除了最后一个元素的数组(注意:数组本身不会被修改),这个数组剩下的元素其实就是当前处理模块的祖先模块们,将这个数组进行 reduce 处理,累计值初始为 state(也就是 store.state),最后返回父链。
举个栗子:
如果 path 为 [],即当前处理模块为根模块,经过 reduce 后返回 state
如果 path 为 [a, c],经过 reduce 后返回 state.a,最终 c 模块的 state 会被挂载在 state.a.c.state 上面
挂载代码如下:
Vue.set(parent, path[path.length - 1], module.state)
对于上面的例子,即:Vue.set(state.a, 'c', c.state)
另外,使用 Vue.set 是为了保证数据响应式。
以上,我们的简易版 Vuex 就实现完了。
另外,值得一提的是:在我们的代码中,直接通过 let modules = options 取得了根模块,而在 Vuex 源码中实际还有一个模块收集的过程,这个方法会将模块收集成一个如下的树结构
{
  _raw: {...},
  _children: {...},
  state: {...}
}
_raw 表示 options 传入的模块的原生形式,_children 中包含了该模块的子模块,state 为该模块的 state 状态
感兴趣的话可以翻阅 Vuex 源码,或者看看笔者的实现。
感谢阅读,若以上讲述有所纰漏还望指正。










