vuex 源码解析

目录分析

# 首先 clone 一下项目
git clone https://github.com/vuejs/vuex.git

# 然后进入源码目录
cd ./vuex/src
# 源码目录结构
.
├── helpers.js          // 提供 action、 mutations 以及 getters 的查找 API
├── index.esm.js
├── index.js            // 源码主入口文件
├── mixin.js            // 提供 store 在 Vue 实例上的装载注入
├── module              // 提供 module 对象与 module 对象树的创建功能
│     ├── module-collection.js
│     └── module.js
├── plugins             // 提供开发辅助插件
│     ├── devtool.js
│     └── logger.js
├── store.js
└── util.js             // 提供工具方法

源码分析

index.js

// 源码的入口文件,提供 store 的各个 module 的构建安装

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


// 从这里就可以看出,我们可以使用 vuex 的什么方法与属性,如最基本的 Store
// 供给 Vue.use() 全局挂载的 install
// 展开内容的 mapState, mapMutations, mapGettrs, mapActions
// 创建基于命名空间的组件绑定辅助函数 createNamespacedHelpers

export default {
  Store,
  install,
  version: '__VERSION__',
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers
}

helpers.js

/**
 * mapState 方法,方便快速取值的,竟然大家都看 vuex 源码了,它肯定很熟悉了吧
 */
export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  // 把要取的内容序列化成指定的格式,然后遍历执行回调,并赋值给 res[key]
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      let state = this.$store.state
      let getters = this.$store.getters
      // 如果是存在命名空间,那就表示使用了模块,在模块内找内容,否则在全局 $store 中查找内容
      if (namespace) {
        // 根据模块名称在顶层的 $store 中找到模块的内容
        // 如果不存在就直接返回
        // 如果存在取出模块的 state, getters 存储在变量 state, getters 中
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      // 如果要取的是 getters 中的内容,那就表示它调用的是一个函数,直接函数并返回结果,否则就是取 state 中的内容,直接返回内容
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // 标记在 devtools 中的 vuex key,将其 flag 调为 true
    res[key].vuex = true
  })
  // 返回 res 的内容,因为 res 是引用类型,虽然声明的时候使用了 const 但是还是可以为其添加内容的,毕竟我们没有改它的对象地址
  return res
})

/**
 * mapMutations 方法
 */
export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  // 序列化好参数,将其转换为指定格式然后 forEach 遍历
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // 从根目录的 $store 上拿到 commit 存储到 commit 上
      let commit = this.$store.commit
      // 处理命名空间的情况,如果存在命名空间,则调整参数
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      // 如果传入的 val 是一个函数,那么执行这个函数,并返回结果
      // 否则就执行 commit
      // 这里使用的是 apply 是因为 apply 最合适处理传入不确定数量的参数的情况
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

/**
 * mapGetters 方法
 */
export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  // 把所有传入的参数序列化,然后调用 forEach 进行处理
  normalizeMap(getters).forEach(({ key, val }) => {
    // 如果有命名空间,那么会自动加上,如果没有命名空间,会加上 '' ,这样其实没有改变
    val = namespace + val
    res[key] = function mappedGetter () {
      // 如果命名空间存在,但是没有找到,这个时候就直接返回一个 undefined
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      // 如果现在不是线上环境且 val 并不在 $store 的列表中,那么就报错
      if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      // 错误情况全部排除了,这个时候我们就可以返回指定的 getters 的计算内容了
      return this.$store.getters[val]
    }
    // 标记在 devtools 中的 vuex key,将其 flag 调为 true
    res[key].vuex = true
  })
  return res
})

/**
 * mapActions 方法,这里就不做解释了,它和 mapMutations 很像,区别是执行的是 commit, dispatch 而已
 * 和 mapMutations 的主要区别是它可以执行异步方法
 * 其实 mapMutations 也可以执行异步方法,但是它为了更好的实现编程思想造成的
 */
export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      let dispatch = this.$store.dispatch
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
        if (!module) {
          return
        }
        dispatch = module.context.dispatch
      }
      return typeof val === 'function'
        ? val.apply(this, [dispatch].concat(args))
        : dispatch.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

/**
 * createNamespacedHelpers 创建基于命名空间的组件绑定辅助函数
 * 用于快速为命名空间生成 mapState, mapGetters, mapMutations, mapActions 等属性
 */
export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})

/**
 * 把内容序列化成一个 Map 的形式,返回一个数组,方便调用,传入参数只能是数组或者对象
 * 序列化成 { key: keyName, val: value} 的形式在数组中,这样使用 filter, forEach, every, any 等方法的时候就比较方便了
 */
function normalizeMap (map) {
  // 这里是通过检查是否是数组来判断能不是能进行 map 操作来直接转换
  // 如果是数组,那就就用 map 方法进行直接转换
  // 如果不是数组,那么就是对象,就将对象的属性名使用 Object.keys() 方法来将其属性名整合成一个数组,再利用数组的 map 方法转换
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

/**
 * 这里一个函数式编程的经典实例,首先传入回调,并返回一个函数
 * 返回的函数我们可以存在一个新的变量中,然后执行,以后就只用传入 namespace 与 map 就可以了, fn 已经常驻内存中
 */
function normalizeNamespace (fn) {
  return (namespace, map) => {
    // 这里是调节参数,是用于处理没有传入 namespace 的情况的
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
    // 如果 namespace 最后一位不是 '/' 那么为了以后的处理方便,添加上 '/'
      namespace += '/'
    }
    // 返回执行回调的结果,注:这里调用 normalizeNamespace 返回的值才返回的,此时 fn 已经常驻在内存中了
    return fn(namespace, map)
  }
}

/**
 * 拿到模块名拿到对应的模块
 */
function getModuleByNamespace (store, helper, namespace) {
  // 从 store 的 map 中找到对应的模块存储在 module 中,如果有内容就是一个对象,如果没有内容,那么则是 undefined
  const module = store._modulesNamespaceMap[namespace]
  // 生产环境下不报错,如果是开发环境,且 module 不存在的话,那还是会报错的
  if (process.env.NODE_ENV !== 'production' && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  // 返回找到的结果
  return module
}

mixin.js

export default function (Vue) {
  // 取大版本号 一般 Vue 的版本号者是 X.X.X 形式的,这样就可以取到第一位的大版本号
  const version = Number(Vue.version.split('.')[0])

  // 如果大版本号大于等于 2 ,那就表示 Vue 拥有了 mixin 方法
  // 这样我们就可以直接调用它,刚 vuexInit 添加到 beforeCreate 钩子函数中
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 如果使用的是旧版本的 Vue 那么没有 mixin ,要使用 _init 方法来调用
    // 先把 Vue.prototype._init 存储在常量中,之所以使用常量,就是为了防止不小心修改
    // 然后扩充 Vue.prototype._init 方法,在 options 中添加属性内容 vuexInit
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      // 如果 options.init 之前有参数,那就就将参数拼接起来来
      // 如果没有参数,那么就将其当今参数传入
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      // 然后执行原来的 Vue.prototype._init 方法,从可以我们可以看出
      // 这次的扩充函数其实主要就是添加了参数设置,在执行步骤上没有任何的改变
      _init.call(this, options)
    }
  }

  /**
   * 内部函数,就是使 store 在 Vue 上 mixin 的方法
   */
  function vuexInit () {
    // 先保存系统参数,存为常量,防止误修改
    const options = this.$options
    // 如果系统参数中存在 store 那就检查其是否为 store 对象的实例
    // 如果是的话那就将其挂载在 this.$store 上
    // 如果不是的话,那么就实例化一个 store 对象并挂载在 this.$store 上
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 如果当前参数没有没有 store 对象,但是有 parent 对象,那就说明它依赖于其父组件
      // 那么将它的父组件的 store 挂载在 this.$store 上
      this.$store = options.parent.$store
    }
  }
}

store.js

store.js 其实就是 vuex 的核心部分,这部分代码我们可以分几部分来看

class Store


export class Store {
  // 构造函数,默认传入 {}
  constructor (options = {}) {
    // 如果当前环境是浏览器环境,且没有安装 vuex ,那么就会自动安装
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    // 如果是开发环境那么进行断言检测,以保证程序的稳定
    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

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

    // 初始化一些参数
    this._committing = false                             // 是否在进行提交状态标识
    this._actions = Object.create(null)                  // acitons 操作对象
    this._actionSubscribers = []                         // action 订阅列表
    this._mutations = Object.create(null)                // mutations操作对象
    this._wrappedGetters = Object.create(null)           // 封装后的 getters 集合对象
    this._modules = new ModuleCollection(options)        // vuex 支持 store 分模块传入,存储分析后的 modules
    this._modulesNamespaceMap = Object.create(null)      // 模块命名空间 map
    this._subscribers = []                               // 订阅函数集合
    this._watcherVM = new Vue()                          // Vue 组件用于 watch 监视变化

    // 替换 this 中的 dispatch, commit 方法,将 this 指向 store
    const store = this
    const { dispatch, commit } = this
    // 其实也可以这么写 
    // this.dispatch = dispatch.bind(store)
    // this.commit = commit.bind(store)
    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

    // 数据树
    const state = this._modules.root.state

    // 加载安装模块
    installModule(this, state, [], this._modules.root)

    // 重置虚拟 store
    resetStoreVM(this, state)

    // 如果使用了 plugins 那么挨个载入它们
    plugins.forEach(plugin => plugin(this))

    // 如果当前环境安装了开发者工具,那么使用开发者工具
    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }

  // 获取 state, 是从虚拟 state 上获取的,为了区别,所以使用的是 $$state
  get state () {
    return this._vm._data.$$state
  }

  set state (v) {
    // 如果是开发环境那么进行断言检测,以保证程序的稳定
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `Use store.replaceState() to explicit replace store state.`)
    }
  }

  commit (_type, _payload, _options) {
    // 先统一一下参数,方便后续处理
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options)

    const mutation = { type, payload }
    const entry = this._mutations[type]
    // 如果在 mutations 列表中不存在当前指定的方法,那么表示传入方法错误,直接返回,开发环境会报错
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    // 专用修改 state 的方法
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })
    // 遍历执行订阅者函数,并传入当前设置以执行指定的订阅者
    this._subscribers.forEach(sub => sub(mutation, this.state))

    // 如果是开发环境,那么当 options 与 options.silent 都存在的情况下,进行报警
    if (
      process.env.NODE_ENV !== 'production' &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

  dispatch (_type, _payload) {
    // 先统一一下参数,方便后续处理
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    // 如果在 action 列表中不存在当前指定的方法,那么表示传入方法错误,直接返回,开发环境会报错
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }
    // 遍历执行订阅者函数,并传入当前设置以执行指定的订阅者
    this._actionSubscribers.forEach(sub => sub(action, this.state))

    // 如果有多个 entry 那么,使用 Promise.all() 来执行,并返回结果
    // 如果只有一个 entry 那么稳了,就执行第一个就可以了
    return entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)
  }

  // 将函数加入到订阅列表中
  subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
  }

  // action 的订阅
  subscribeAction (fn) {
    return genericSubscribe(fn, this._actionSubscribers)
  }

  // 监视数据
  watch (getter, cb, options) {
    // 如果是开发环境那么断言检测一下,以保证程序稳定
    if (process.env.NODE_ENV !== 'production') {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    // 使用 $watch 来监视 getter 的数据状态
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }

  // 修改 state
  replaceState (state) {
    // 唯一合法修改 state 的方式
    this._withCommit(() => {
      this._vm._data.$$state = state
    })
  }

  // 注册模块
  registerModule (path, rawModule, options = {}) {
    // 进行参数处理
    if (typeof path === 'string') path = [path]
    // 如果是开发环境那么断言检测一下,以保证程序稳定
    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
      assert(path.length > 0, 'cannot register the root module by using registerModule.')
    }

    // 注册模块
    this._modules.register(path, rawModule)
    // 把模块安装到 state 上面
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // 重置虚拟 store
    resetStoreVM(this, this.state)
  }

  // 取消模块注册
  unregisterModule (path) {
    // 处理一下参数
    if (typeof path === 'string') path = [path]
    // 如果是开发环境那么断言检测一下,以保证程序稳定
    if (process.env.NODE_ENV !== 'production') {
      assert(Array.isArray(path), `module path must be a string or an Array.`)
    }

    // 取消模块注册
    this._modules.unregister(path)
    // 拿到模块,并将其从其父模块上删除
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      Vue.delete(parentState, path[path.length - 1])
    })
    // 重置模块,也就是重新安装
    resetStore(this)
  }

  // 热更新
  hotUpdate (newOptions) {
    // 升级模块,然后重新载入模块
    this._modules.update(newOptions)
    resetStore(this, true)
  }

  // 在 commit 的时候执行,主要是修改 committing 状态,执行回调,修改内容,再将 committing 状态改回去
  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }
}

functions

// 通用订阅,返回一个函数,这个函数是用于从 subs 中删除插入的 fn 的
function genericSubscribe (fn, subs) {
  // 如果 fn 不存在于 subs 列表中,那么添加进 subs 中,否则不操作
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}

// 重置 store
function resetStore (store, hot) {
  // 先将几个重要的对象清空
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // 调用 installModule 重新安装模块
  installModule(store, state, [], store._modules.root, true)
  // 调用 resetStoreVM 重围 VM
  resetStoreVM(store, state, hot)
}

/**
 * resetStoreVM
 * @param  {[type]} store     store 表示当前 Store 实例
 * @param  {[type]} state     module 表示当前安装的模块
 * @param  {[type]} hot       hot 当动态改变 modules 或者热更新的时候为 true
 */
function resetStoreVM (store, state, hot) {
  // 先备份旧有的 vm ,以便以后使用
  const oldVm = store._vm

  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 通过Object.defineProperty为每一个getter方法设置get方法
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true
    })
  })

  // 先将 Vue.config.silent 存储在临时常量中,这样 new Vue 实例的时候不会报错
  const silent = Vue.config.silent
  Vue.config.silent = true
  // 实例化一个新的 Vue 对象
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  // 改成原先的状态
  Vue.config.silent = silent

  // 如果使用的是严格模式,那么调用 enableStrictMode 来对 store 进行处理
  if (store.strict) {
    enableStrictMode(store)
  }

  // 如果 oldVm 存在,那么先判断是否传入了 hot 如果传入了,就先将其 $$state 设为 null . 然后在调用 nextTick 来删除 oldVm 实例
  if (oldVm) {
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

/**
 * installModule
 * @param  {[type]} store     store 表示当前 Store 实例
 * @param  {[type]} rootState rootState 表示根 state
 * @param  {[type]} path      path 表示当前嵌套模块的路径数组
 * @param  {[type]} module    module 表示当前安装的模块
 * @param  {[type]} hot       hot 当动态改变 modules 或者热更新的时候为 true
 */
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  // 获取模块的命名
  const namespace = store._modules.getNamespace(path)

  // 把当前模块名加入的 store 的 moduleNamespaceMap 中
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // 如果当前不是子模块也不是热更新状态,那么就是新增子模块,这个时候要取到父模块
  // 然后插入到父模块的子模块列表中
  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, action, getters, child 注册
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

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

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

  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

/**
 *  创建本地上下文环境
 */
function makeLocalContext (store, namespace, path) {
  // 是否使用了命名空间
  const noNamespace = namespace === ''

  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
        // 如果是开发环境那且 store._actions[type] 不存在,那么报错
        if (process.env.NODE_ENV !== 'production' && !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
        // 如果是开发环境那且 store._mutations[type] 不存在,那么报错
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

      store.commit(type, payload, options)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

function makeLocalGetters (store, namespace) {
  const gettersProxy = {}

  const splitPos = namespace.length
  Object.keys(store.getters).forEach(type => {
    if (type.slice(0, splitPos) !== namespace) return
    const localType = type.slice(splitPos)
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

/**
 *  registerMutation
 *  store为当前 Store 实例
 *  type为 mutation 的 key
 *  handler 为 mutation 执行的回调函数
 *  local 为当前模块的路径
 */
function registerMutation (store, type, handler, local) {
  // 首先通过 type 拿到对应的 mutations 数组,如果没有则使用空数组
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 把 wrappedMutationHandler push 到这个数组中
  // 这个函数接收一个参数 payload,这个就是我们在定义 mutation 的时候接收的额外参数
  // 这个函数执行的时候会调用 mutation 的回调函数
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

/**
 *  registerAction
 *  store为当前 Store 实例
 *  type为 action 的 key
 *  handler 为 action 执行的回调函数
 *  local 为当前模块的路径
 */
function registerAction (store, type, handler, local) {
  // 首先通过 type 拿到对应的 actions 数组,如果没有则使用空数组
  const entry = store._actions[type] || (store._actions[type] = [])
  // 把 wrappedMutationHandler push 到这个数组中
  // 这个函数接收一个参数 payload,这个就是我们在定义 action 的时候接收的额外参数
  // 这个函数执行的时候会调用 action 的回调函数 cb
  entry.push(function wrappedActionHandler (payload, cb) {
    // 执行 handler 并将结果存储在 res 中
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    // 如果 res 是一个 Promise 对象,那么调用 resolve 方法 
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    // 如果使用了开发者工具的话,那么出错的时候,触发开发者工具的 vuex:error 事件
    // 没有使用的话直接返回 res
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

/**
 *  registerGetter
 *  store为当前 Store 实例
 *  type为 getter 的 key
 *  rawGetter 为 getter 执行的回调函数
 *  local 为当前模块的路径
 */
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    // 如果是开发环境,那么报错
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state,
      local.getters,
      store.state,
      store.getters
    )
  }
}

// 监测 store._vm.state 的变化
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    // 如果是开发环境,那么进行断言检测,以保证程序稳定
    if (process.env.NODE_ENV !== 'production') {
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

/*
 * getNestedState 根据 path 查找 state 上的嵌套 state
 */
function getNestedState (state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}

/**
 *  统一对象风格
 */
function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }
  // 如果是开发环境,那么进行断言检测,以保证程序稳定
  if (process.env.NODE_ENV !== 'production') {
    assert(typeof type === 'string', `Expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}

install


export function install (_Vue) {
  // 如果 Vue.use(vuex) 已经调用过了,那么就不执行操作,且在开发环境下会报错
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  // 调用 applyMixin 方法来初始化 vuex
  applyMixin(Vue)
}

util.js

/**
 * 从一个数组中找到一个符合条件的元素
 * 参数是一个数组和一个回调,回调用于检查元素是否符合要求
 * 如果有符合条件的元素,返回第一个元素,否则返回 undefined
 * let arr = []
 * console.log(arr.filter(el => el > 0))     // undefined
 * arr = [4,5,6,7]
 * console.log(arr.filter(el => el > 0))     // 4
 */
export function find (list, f) {
  return list.filter(f)[0]
}

/**
 * 深拷贝方法,默认有一个缓存参数,初始时为空数组,向下递归的时候,会不断的添加内容
 */
export function deepCopy (obj, cache = []) {
  // 如果传入的内容为 null 或者 不为 object 那么就表示到最底层了
  // 当前拷贝是一个基础的数据类型,此时直接返回内容,停止递归
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // 如果对象在缓存中找到的话,那就直接返回缓存对象
  // 因为虽然是深拷贝,但是原对象中的某几个属性同时引用了某个对象的话
  // 这个时候为了与之前对象保持一致,不应该进行深拷贝,而是直接传递引用,比如函数什么的
  const hit = find(cache, c => c.original === obj)
  if (hit) {
    return hit.copy
  }

  // 判断当前拷贝的是数组还是对象,然后生成对应的类型,然后将当前对象传入到缓存中
  const copy = Array.isArray(obj) ? [] : {}
  cache.push({
    original: obj,
    copy
  })

  // 对对象的每一个属性进行深拷贝处理,这里是用递归
  Object.keys(obj).forEach(key => {
    copy[key] = deepCopy(obj[key], cache)
  })

  // 返回内容
  return copy
}

/**
 * 给对象方法一个 forEach 方法,核心使用的是 ES6 的 Object.keys() 方法
 * let obj = {
 *   a : 5,
 *   b : 'string',
 *   c : '/reg/',
 *   d : function () { console.log('666')}
 * }
 * let arr = Object.keys(obj)
 * console.log(arr)     // ['a', 'b', 'c', 'd']
 * console.log(Object.prototype.toString.call(arr))    // [object Array]
 */
export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

/**
 * 判断参数是不是一个对象,返回 true,false
 * JavaScript 是通过前三位的二进制码来判断类型的, Object 的前三位是 000 . 
 * 但是 null 是全 0 。这是一个历史遗留且不好修改的 BUG,故要多添加此层进行判断
 * 也可以用 Object.prototype.toString.call(obj) === '[object Object]' 来进行判断
 */
export function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

/**
 * 判断一个对象是不是 Promise 对象,只要检查它是不是有 then 方法即可。
 * 因为 Promise 的所有返回内容都是一个新的 Promise 对象,所以恒有 then 方法
 * 也可以用 Object.prototype.toString.call(val) === '[object Promise]'
 */
export function isPromise (val) {
  return val && typeof val.then === 'function'
}

/**
 * 断言方法,用于检查传入内容是否正确,
 * 如果正常则继续执行, 不正确就抛出异常,这是保证程序正常运行的一种手段
 */
export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}

module.js

import { forEachValue } from '../util'

// Module 类
export default class Module {
  // 构造函数
  constructor (rawModule, runtime) {
    this.runtime = runtime
    this._children = Object.create(null)
    this._rawModule = rawModule
    const rawState = rawModule.state

    // 如果 rowSTate 是一个 function 那么就执行它,否则就取它的值。如果它没有值的话,为了程序的稳定,给它一个默认值 {}
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  // 判断是否是命名空间,如果是则返回 true ,如果不是则返回  false, 这里用到了隐匿类型转换
  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]
  }

  // 更新模块
  update (rawModule) {
    // 修改实例的 namespaced
    this._rawModule.namespaced = rawModule.namespaced
    // 如果更新后的 module 存在 actions, mutations, getters 那么就更新一下内容
    if (rawModule.actions) {
      this._rawModule.actions = rawModule.actions
    }
    if (rawModule.mutations) {
      this._rawModule.mutations = rawModule.mutations
    }
    if (rawModule.getters) {
      this._rawModule.getters = rawModule.getters
    }
  }

  // 给每个子模块执行指定回调,这里使用了 util 中的 forEachValue 方法来实现
  forEachChild (fn) {
    forEachValue(this._children, fn)
  }

  // 对每一个 getter 执行指定回调
  forEachGetter (fn) {
    // 如果存在就执行指定回调,不存在就不费什么事了,直接返回
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }

  // 对每一个 action 执行指定回调
  forEachAction (fn) {
    // 如果存在就执行指定回调,不存在就不费什么事了,直接返回
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }

  // 对每一个 mutation 执行指定回调
  forEachMutation (fn) {
    // 如果存在就执行指定回调,不存在就不费什么事了,直接返回
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
}

module-collection.js

import Module from './module'
import { assert, forEachValue } from '../util'

export default class ModuleCollection {
  // 构造函数
  constructor (rawRootModule) {
    // 注册模块
    this.register([], rawRootModule, false)
  }

  // 拿到模块
  get (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 方法,这里容易引起疑惑的是模块里也叫这个名字啊,怎么办,这里其实调用的是下面的 update 方法
    // 如果要调用本方法进行递归的话,只会使用 this.update()
    update([], this.root, rawRootModule)
  }

  // 注册模块
  register (path, rawModule, runtime = true) {
    // 如果当前非线上环境,那么使用断言来检查它是不是合法的模块
    if (process.env.NODE_ENV !== 'production') {
      assertRawModule(path, rawModule)
    }

    // 实例化一个模块,参数就是要注册的模块
    const newModule = new Module(rawModule, runtime)
    // 如果是根模块,那么就挂载在 this.root 上面
    if (path.length === 0) {
      this.root = newModule
    } else {
      // 如果不是根模块,那么取它的父模块,将其添加到其父模块下
      const parent = this.get(path.slice(0, -1))
      parent.addChild(path[path.length - 1], newModule)
    }

    // 如果实例化的对象还有子模块,那么使用 forEachValue 递归注册其所有的子孙模块
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }

  // 取消模块的注册
  unregister (path) {
    // 取出模块的父模块,因为使用了模块,所以最上面是 root ,肯定是位于第二级,所以不用担心这里会出问题
    const parent = this.get(path.slice(0, -1))
    const key = path[path.length - 1]
    // 如果当前模块正在执行,那么不能取消注册,否则就取消注册
    if (!parent.getChild(key).runtime) return

    parent.removeChild(key)
  }
}

// 升级模块
function update (path, targetModule, newModule) {
  // 如果当前环境不是线上环境,那就代表是开发环境,执行模块断言,检查是否错误
  // 之所以这么做,是因为线上环境很多报错与警告会取消
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, newModule)
  }

  // 调用指定模块的 update 方法,并将新的模块传入
  targetModule.update(newModule)

  // 对新模块的子模块进行遍历操作
  if (newModule.modules) {
    for (const key in newModule.modules) {
      if (!targetModule.getChild(key)) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      // 递归遍历子模块
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
}

// 函数断言对象,包含两个属性,一个是用于判断传入内容是否是函数的方法,另一个是说明
const functionAssert = {
  assert: value => typeof value === 'function',
  expected: 'function'
}

// 对象断言对象,包含两个属性,一个是用于判断传入内容是否是函数或者是一个有属性方法叫 handler 的对象,另一个是说明
const objectAssert = {
  assert: value => typeof value === 'function' ||
    (typeof value === 'object' && typeof value.handler === 'function'),
  expected: 'function or object with "handler" function'
}

const assertTypes = {
  getters: functionAssert,
  mutations: functionAssert,
  actions: objectAssert
}


function assertRawModule (path, rawModule) {
  Object.keys(assertTypes).forEach(key => {
    // 如果在模块中不存在 getters, mutations, actions 那么就直接返回
    // 有的话,就继续进行操作
    if (!rawModule[key]) return

    // 给对应的断言方法起个别名,类似于 java 中的反射,这样的话,在下面的操作中,就可以使用同一个名字了
    const assertOptions = assertTypes[key]

    // 挨个进行断言操作,如果不正确,则生成错误信息并显示
    forEachValue(rawModule[key], (value, type) => {
      assert(
        assertOptions.assert(value),
        makeAssertionMessage(path, key, type, value, assertOptions.expected)
      )
    })
  })
}

// 生成断言信息,如果错误的情况下,那么就返回它。这里其实就是简单的拼接字符串
function makeAssertionMessage (path, key, type, value, expected) {
  let buf = `${key} should be ${expected} but "${key}.${type}"`
  if (path.length > 0) {
    buf += ` in module "${path.join('.')}"`
  }
  buf += ` is ${JSON.stringify(value)}.`
  return buf
}

devtool.js

// 检查是否是浏览器环境,且浏览器下是否挂载 vue 的 devtools,如果存在就挂载在 devtoolHook 上面,否则给一个 undefined
const devtoolHook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  // 如果不存在 vue devtools ,那么直接返回,不进行操作
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook

  // 触发 devtool 的 vuex:init 事件,并传入 store 。这样是用初始化
  devtoolHook.emit('vuex:init', store)

  // 监听 vuex:travel-to-state 方法,用于更换 store 状态
  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  // 订阅 store 的 mutation 事件,如果触发了 mutation 事件,那么就执行回调
  // 回调是触发 devtool 的 vuex:mutations 方法
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}

logger.js

import { deepCopy } from '../util'

// 先给参数赋予默认值,这样方便在参数缺省的情况下执行不会出错
export default function createLogger ({
  collapsed = true,
  filter = (mutation, stateBefore, stateAfter) => true,
  transformer = state => state,
  mutationTransformer = mut => mut,
  logger = console
} = {}) {
  // 从这里我们可以看出,这里利用了函数式编程,返回了一个函数,当我们执行 logger 的时候,其实就是调用这个函数
  return store => {
    // 深拷贝了 store.state 这么做的原因是用于和后来状态进行对比,毕竟 logger 系统就是用于干这个的
    let prevState = deepCopy(store.state)

    // 给 sotre 的 mutation 事件添加订阅,如果触发,执行下面传入的函数
    store.subscribe((mutation, state) => {
      // 先判断 console 是否存在,如果不存在,那就 GG 了,没法下去了,直接返回
      if (typeof logger === 'undefined') {
        return
      }
      // 深拷贝一下 state ,也就是存储一下当前的状态
      const nextState = deepCopy(state)

      // 根据传入 filter() 判断,默认为 true 直接执行
      if (filter(mutation, prevState, nextState)) {
        // 初始化一些参数,如时间,格式文本函数,信息等
        const time = new Date()
        const formattedTime = ` @ ${pad(time.getHours(), 2)}:${pad(time.getMinutes(), 2)}:${pad(time.getSeconds(), 2)}.${pad(time.getMilliseconds(), 3)}`
        const formattedMutation = mutationTransformer(mutation)
        const message = `mutation ${mutation.type}${formattedTime}`
        const startMessage = collapsed
          ? logger.groupCollapsed
          : logger.group

        // 调用 console.log(groupCollapsed) 输出内容,如果出问题,不用管,直接处理输出 message
        try {
          startMessage.call(logger, message)
        } catch (e) {
          console.log(message)
        }

        // 具体的打印内容
        logger.log('%c prev state', 'color: #9E9E9E; font-weight: bold', transformer(prevState))
        logger.log('%c mutation', 'color: #03A9F4; font-weight: bold', formattedMutation)
        logger.log('%c next state', 'color: #4CAF50; font-weight: bold', transformer(nextState))

        // 执行 console.groupEnd() 其实这里面是和上面的那个异常处理是对应的,同理,出了问题执行 console.log()
        try {
          logger.groupEnd()
        } catch (e) {
          logger.log('—— log end ——')
        }
      }

      // 把当初状态存储在常驻变量中,方便下次与新的状态比较
      prevState = nextState
    })
  }
}

/**
 * 字符串重复方法,返回指定字符串多次重复后的结果
 * 在 ES6 中已经可以写成 return str.repeat(times)
 */
function repeat (str, times) {
  return (new Array(times + 1)).join(str)
}

/**
 * 在数字前面补 0 到指定位数,并返回结果
 * 在 ES6 中已经可以写成 return num.toString().padStart(maxLength, '0')
 * 在 ES5 中也可以写成 return (repeat('0', maxLength) + num).slice(-maxLength)
 */
function pad (num, maxLength) {
  return repeat('0', maxLength - num.toString().length) + num
}

你可能感兴趣的:(javascript学习笔记,vuejs学习笔记)