让前端飞Web前端之路饥人谷技术博客

手把手讲解:Vuex 剖析与简单实现

2019-12-15  本文已影响0人  临安linan

更多个人博客:https://github.com/zenglinan/blog

目录

  1. install 方法
  2. Store 类

闲话不多说,让我们开始实现一个简单的 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' 导出来的应该是一个对象,上面有至少两个属性:installStoreinstall 方法在 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 被外部直接修改。

另外要注意的一个点是:我们借用 Vuestate 变成响应式数据了。这样的话,当 state 变化的时候,依赖的地方都能得到更新通知

以上,我们简单实现了 State 状态

(2) Getters

接下来实现 GettersGetters 是通过 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

对于模块的 gettersmutationsactions,都被定义到 store 上了,通过形如 store.getters.xxx 这种方式访问

好了,现在目标明确了,接下来就把上面提到的两个点实现:

首先,我们需要将 storegettersmutationsactions 进行初始化成空对象

我们不再需要用 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 等属性的收集处理

先解释一下传给这个函数的四个参数:

  1. store 实例
  2. store 上的 state
  3. path:在 installModule 中需要递归来安装处理子模块,所以用 path 数组来表示模块的层级关系。数组的最后一位为当前要处理的模块的模块名,最后一位前面的都是当前模块的祖先模块。举个栗子:如果是根模块,传入的 path[],如果传入 [a, c],说明当前处理模块名为 c,模块层级为:根模块 > a模块 > c模块
  4. 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.xxxstore.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 源码,或者看看笔者的实现

感谢阅读,若以上讲述有所纰漏还望指正。

上一篇 下一篇

猜你喜欢

热点阅读