(一)小菜鸡趁热打铁Vuex源码阅读

281 阅读7分钟

vue2.x源码刚粗略阅读完,说实话模板编译深层的实在是一时看不完,其他的差不多了解了,想着既然都看了vue了那把全家桶都看了吧,vuex拉了源码,感觉这代码量看着就舒服多了,先把这个解决了,后续再看vue-router,vuex阅读的是3.6.2版本。

0.目录结构

代码是真不多,核心代码就更少了,感觉很适合刚学会看源码的小白,最主要的是store.js中的Store构造函数

image.png

1.入口

index.js

老规矩,先看咱们使用的是import拿到的是什么东西

import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
import createLogger from './plugins/logger'

// 默认导出
export default {
  // store构造函数
  Store,
  // Vue.use()的时候执行函数
  install,
  version: '__VERSION__',
  
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}
// 按需导出
export {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}

2.Vue.use(Vuex)

先看一下Vuex官网给出的开始方式

import Vue from 'vue'
import Vuex from 'vuex'
// 注册插件
Vue.use(Vuex)
const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    increment (state) {
      state.count++
    }
  }
})

通过阅读Vue源码,或者文档我们知道调用Vue.use()

  • 参数是函数,直接执行他
  • 参数是对象的时候,执行对象中install方法

install

// src/store.js
import applyMixin from './mixin'

// ...
export function install (_Vue) {
 if (Vue && _Vue === Vue) {
   if (__DEV__) {
     console.error(
       '[vuex] already installed. Vue.use(Vuex) should be called only once.'
     )
   }
   return
 }
 // 注册后拿到Vue变量,且只能注册一次
 Vue = _Vue
 applyMixin(Vue)
}

mixin

Vue1.x2.x以上的版本做了判断,可能是1.x没有Vue.mixin这个静态方法?这里不做深究,主要讨论2.x及以上版本

  1. 混入了一个beforeCreate的钩子函数
  2. 函数将会在new Vue(options)时拿到中的options参数中的store并挂载到了vm实例上vm.$store
// src/mixin.js
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  function vuexInit () {
    const options = this.$options
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

3.new Store()

Store类

constructor

  1. 初始化内部使用的一些变量
  2. call重新封装了dispatchcommit方法,使得调用的时候强制this指向当前实例
  3. 通过参数对象生成一个模块树,返回收集模块时的对象,该对象将暴露一个root属性
  4. 初始化modules
  5. 判断是否strict在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误
  6. resetStoreVM:让stategetter响应式
  7. 注册插件
  8. devtools开关
export class Store {
  constructor (options = {}) {
   
    // ...

    const {
      plugins = [],
      strict = false
    } = options

    // 初始化一些内部使用的变量
    this._committing = false
    this._actions = Object.create(null)
    this._actionSubscribers = []
    this._mutations = Object.create(null)
    this._wrappedGetters = Object.create(null)
    // 该创建的对象包含一个root属性,指向了模块树的根
    this._modules = new ModuleCollection(options)
    this._modulesNamespaceMap = Object.create(null)
    this._subscribers = []
    this._watcherVM = new Vue()
    this._makeLocalGettersCache = Object.create(null)

    // 这里封装了dispatch和commit方法,使得this强制指向store
    const store = this
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

    // 严格模式
    this.strict = strict
    // 拿到根节点的state,这一步构建好了一棵树
    const state = this._modules.root.state
    // 将树递归遍历,挂载到store的私有属性,可以点运算符访问
    installModule(this, state, [], this._modules.root)
    // 使得数据响应式
    resetStoreVM(this, state)
    plugins.forEach(plugin => plugin(this))
    
    // 开启devtools开关
    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      devtoolPlugin(this)
    }
  }
}

ModuleCollection类

我们知道store是可以用模块化来创建的,如何通过输入的options对象来创建模块化的store呢,首先应该有一个Module类,然后需要有一个收集的过程,就是通过这个Modulecollection

构造函数

  • constructor: 调用registeroptions根开始递归注册 方法
  • get(path): path是一个数组,记录着变量访问的key值,这里巧妙的运用了Array.prpototype.reduce方法来迭代寻找值
  • getNamespace:通过模块路径生成一个用/分隔的命名空间
  • update
  • register: 注册模块,实际上是一个生成树的过程,通过原始对象不断递归遍历,new Module生成节点,再将其添加到父节点上
  • unregister
  • isRegistered
// src/moduel/module-collection.js
export default class ModuleCollection {
  constructor (rawRootModule) {
    // 根options
    this.register([], rawRootModule, false)
  }

  
  get (path) {
     // this.root实际上是整个参数元素,通过path中存在的key值来迭代找到path所在模块对象
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }

  getNamespace (path) {
    let module = this.root
    return path.reduce((namespace, key) => {
      module = module.getChild(key)
      return namespace + (module.namespaced ? key + '/' : '')
    }, '')
  }

  update (rawRootModule) {
    update([], this.root, rawRootModule)
  }

  register (path, rawModule, runtime = true) {
    // ...
    
    // 通过当前参数对象来新建一个module实例
    const newModule = new Module(rawModule, runtime)
    // 路径数组是空,说明是根模块
    if (path.length === 0) {
      this.root = newModule
    } else {
      // 找到父模块
      const parent = this.get(path.slice(0, -1))
      // 将当前模块继承父模块
      parent.addChild(path[path.length - 1], newModule)
    }

    // 如果当前模块下还存在嵌套模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        // 这里的key实际上是每一个modules的名字,将遍历到的模块名添加到path中作为模块路径的一部分
        // rawChildModule 拿到的是modules[key]这个对象,然后递归
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  unregister (path) {
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    const child = parent.getChild(key)

    // ...
    if (!child.runtime) {
      return
    }

    parent.removeChild(key)
  }

  isRegistered (path) {
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]

    if (parent) {
      return parent.hasChild(key)
    }

    return false
  }
}

export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

Module类

相当于树中的一个节点,通过声明对象参数,来创建一个模块树的结点,代码非常简洁易懂,且该文件也只包含这些代码

构造函数

  • constructor:初始化参数
    • runtime
    • _children
    • _rawModule
    • _state 属性
  • namespaced: 是否有单独的命名空间 方法
  • addChild
  • removeChild
  • getChild
  • hasChild
  • update
  • forEachChild
  • forEachGetter
  • forEachAction
  • forEachMutation
// src/moduel/modules.js
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // 子模块
    this._children = Object.create(null)
    // 原始对象
    this._rawModule = rawModule
    const rawState = rawModule.state

    // 保存state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  get namespaced () {
    return !!this._rawModule.namespaced
  }

  addChild (key, module) {
    this._children[key] = module
  }

  removeChild (key) {
    delete this._children[key]
  }

  getChild (key) {
    return this._children[key]
  }

  hasChild (key) {
    return key in this._children
  }

  update (rawModule) {
    this._rawModule.namespaced = rawModule.namespaced
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

  forEachChild (fn) {
    forEachValue(this._children, fn)
  }

  forEachGetter (fn) {
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }

  forEachAction (fn) {
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }

  forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
}

installModuel

  • 可以通过点运算符来访问state
  • 将输入的参数挂载到store上,并统一格式
function installModule (store, rootState, path, module, hot) {
  // 是否是根木块
  const isRoot = !path.length
  // 是否拥有命名空间
  const namespace = store._modules.getNamespace(path)

  // 注册命名空间
  if (module.namespaced) {
    // 将来我们可以通过命名空间来找到模块
    store._modulesNamespaceMap[namespace] = module
  }

  // 设置state的访问方式
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      // 可以通过点运算符来访问模块
      Vue.set(parentState, moduleName, module.state)
    })
  }
   
  // 为下面的注册创建一个临时的上下文环境,这个环境中可以省略命名空间
  // 某种程度上说是一种封装,防止下面注册的时候都要写命名空间
  const local = module.context = makeLocalContext(store, namespace, path)

  // 注册Mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  // 注册Action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })

  // 注册Getter
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 对child模块递归调用该函数
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

makeLocalContext

返回一个上下文对象包含

  • dispatch
  • commit
  • getter
  • state

dispatchcommit经过了封装,内部调用的时候会自动加上命名空间,所以书写的时候可以省略

function makeLocalContext (store, namespace, path) {
  // 是否定义在全局还是有自己的命名空间
  const noNamespace = namespace === ''

  // 定义一个在当前命名空间下的store,可以自动加上命名空间
  const local = {
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (__DEV__ && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      return store.dispatch(type, payload)
    },

    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {
        type = namespace + type
        if (__DEV__ && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }
  
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

registerMutation、registerAction、registerGetter

将输入的各种数据类型统一格式并注册

function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload)
    // action的返回值必须是promise
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

function registerGetter (store, type, rawGetter, local) {
  // ...
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

resetStoreVM

store里的数据变成和vue一样响应式

  1. 创建一个新的vue实例,挂载到store._vm
  2. getter变成computed
  3. state变成data中的一个属性
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm
  
  store.getters = {}
  store._makeLocalGettersCache = Object.create(null)
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    // 对外暴露getter的访问
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  const silent = Vue.config.silent
  Vue.config.silent = true
  // 创建一个vue实例挂载到store._vm上
  // getter -> computed
  // state -> data
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // ..
}

// src/utils.js
export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}

小结

回顾一下我们用到的storeAPI是什么时候被挂载的

  1. state:在installModuleVue.set(parentState, moduleName, module.state)
  2. getter:在resetStoreVM中创建了vue实例挂载到store._vm再用definePrototype代理store.getter
  3. installModule中同时注册了mutationaction但是这两种我们并不是直接调用,而是通过commitdispatch