vuex 源码解析

源码探索之旅

Posted by Li Yucang on July 21, 2019

vuex 源码解析

Vuex 是专为 Vue 开发的统一状态管理工具。当我们的项目不是很复杂时,一些交互可以通过全局事件总线解决,但是这种观察者模式有些弊端,开发时可能没什么感觉,但是当项目变得复杂,维护时往往会摸不着头脑,如果是后来加入的伙伴更会觉得很无奈。这时候可以采用 Vuex 方案,它可以使得我们的项目的数据流变得更加清晰。本文将会分析 Vuex 的整个实现思路,当是自己读完源码的一个总结。

目录结构

拉下项目源码,当前版本 3.1.1,目录结构如下图,我们需要研究的源码部分在 src 目录下:

  • module:提供对 module 的处理,最后构建成一棵 module tree
  • plugins:和 devtools 配合的插件,提供像时空旅行这样的调试功能。
  • helpers:提供如 mapActions、mapMutations 这样的 api
  • index、index.esm:源码的主入口,抛出 Store 和 mapActions 等 api,一个用于 commonjs 的打包、一个用于 es module 的打包
  • mixin:提供 install 方法,用于注入 $store
  • store:vuex 的核心代码
  • util:一些工具函数,如 deepClone、isPromise、assert

工具函数

在阅读源码之前,我们先熟悉一下会经常出现的工具函数:

/**
 * 从一个数组中找到一个符合条件的元素
 * 参数是一个数组和一个回调,回调用于检查元素是否符合要求
 * 如果有符合条件的元素,返回第一个元素,否则返回 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 = []) {
  // just return if obj is immutable value
  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,故要多添加此层进行判断
 */
export function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}


/**
 * 判断一个对象是不是 Promise 对象,只要检查它是不是有 then 方法即可。
 */
export function isPromise (val) {
  return val && typeof val.then === 'function'
}

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

export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}

源码探索

Vuex 的注册

先从一个简单的示例入手,一步一步分析整个代码的执行过程,下面是官方提供的简单示例:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

Vue 官方建议的插件使用方法是使用 Vue.use 方法,这个方法会调用插件的 install 方法,看看 install 方法都做了些什么,首先找到 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
}

从 index.js 中可以看到 install 方法在 store.js 中抛出,在 store.js 中找到相关代码如下:

let Vue // bind on 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)
}

声明了一个 Vue 变量,这个变量在 install 方法中会被赋值,这样可以给当前作用域提供 Vue,这样做的好处是不需要额外 import Vue from ‘vue’ 不过我们也可以这样写,然后让打包工具不要将其打包,而是指向开发者所提供的 Vue,比如 webpack 的 externals,这里就不展开了。执行 install 会先判断 Vue 是否已经被赋值,避免二次安装。然后调用 applyMixin 方法,在 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 {
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit

      // 然后执行原来的 Vue.prototype._init 方法,从可以我们可以看出
      // 这次的扩充函数其实主要就是添加了参数设置,在执行步骤上没有任何的改变
      _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) {
      // 如果当前参数没有没有 store 对象,但是有 parent 对象,那就说明它依赖于其父组件
      // 那么将它的父组件的 store 挂载在 this.$store 上
      this.$store = options.parent.$store
    }
  }
}

这里会区分 vue 的版本,2.x 和 1.x 的钩子是不一样的,如果是 2.x 使用 beforeCreate,1.x 即使用_init。当我们在执行 new Vue 启动一个 Vue 应用程序时,需要给上 store 字段,根组件从这里拿到 store,子组件从父组件拿到,这样一层一层传递下去,实现所有组件都有$store属性,这样我们就可以在任何组件中通过this.$store 访问到 store。

store 初始化

接下去继续看例子:

// store.js
export default new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    evenOrOdd: state => state.count % 2 === 0 ? 'even' : 'odd'
  },
  actions:  {
    increment: ({ commit }) => commit('increment'),
    decrement: ({ commit }) => commit('decrement')
  },
  mutations: {
    increment (state) {
      state.count++
    },
    decrement (state) {
      state.count--
    }
  }
})

// app.js
new Vue({
  el: '#app',
  store, // 传入store,在beforeCreate钩子中会用到
  render: h => h(Counter)
})

这里是调用 Store 构造函数,传入一个对象,包括 state、actions 等等,接下去看看 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))

    // 如果当前环境安装了开发者工具,那么使用开发者工具
    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      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.`)
    }
  }
  ...
}

首先会判断 Vue 是不是挂载在 window 上,如果是的话,自动调用 install 方法,然后进行断言,必须先调用 Vue.use(Vuex)。必须提供 Promise,这里应该是为了让 Vuex 的体积更小,让开发者自行提供 Promise 的 polyfill,一般我们可以使用 babel-runtime 或者 babel-polyfill 引入。最后断言必须使用 new 操作符调用 Store 函数。

接下去是一些内部变量的初始化:

  • _committing 提交状态的标志,在 _withCommit 中,当使用 mutation 时,会先赋值为 true,再执行 mutation,修改 state 后再赋值为 false,在这个过程中,会用 watch 监听 state 的变化时是否 _committing 为 true,从而保证只能通过 mutation 来修改 state
  • _actions 用于保存所有 action,里面会先包装一次
  • _actionSubscribers 用于保存订阅 action 的回调
  • _mutations 用于保存所有的 mutation,里面会先包装一次
  • _wrappedGetters 用于保存包装后的 getter
  • _modules 用于保存一棵 module 树
  • _modulesNamespaceMap 用于保存 namespaced 的模块

模块收集

接下去的重点是:

this._modules = new ModuleCollection(options)

接下去看看 ModuleCollection 函数都做了什么,代码如下:

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

export default class ModuleCollection {
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    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([], 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

    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
}

再看看 module-collection 用到的 module:

import { forEachValue } from '../util'

// Base data struct for store's module, package with some attribute and method
export default class Module {
  constructor (rawModule, runtime) {
    // 动态加载的模块为true
    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]
  }

  // 更新模块
  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
    }
  }

  // 给每个子模块执行指定回调,这里使用了 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)
    }
  }
}

这里调用 ModuleCollection 构造函数,通过 path 的长度判断是否为根 module,首先进行根 module 的注册,然后递归遍历所有的 module,子 module 添加其父 module 的 _children 属性上,最终形成一棵树。

接着,还是一些变量的初始化,然后绑定 commit 和 dispatch 的 this 指针。

// 绑定commit和dispatch的this指针
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)
}

这里会将 dispath 和 commit 方法的 this 指针绑定为 store,比如下面这样的骚操作,也不会影响到程序的运行。

this.$store.dispatch.call(this, 'someAction', payload)

模块安装

接着是 store 的核心代码

// 这里是module处理的核心,包括处理根module、命名空间、action、mutation、getters和递归注册子module
installModule(this, state, [], this._modules.root)

找到函数所在位置:

function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length
  // 获取模块的命名空间
  const namespace = store._modules.getNamespace(path)

  // 存在命名空间时,加入到 moduleNamespaceMap 中
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    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)
  })
}

首先保存 namespaced 模块到 store._modulesNamespaceMap,再判断是否为根组件且不是 hot,得到父级 module 的 state 和当前 module 的 name,调用 Vue.set(parentState, moduleName, module.state)将当前 module 的 state 挂载到父 state 上。

这里看一眼 getNestedState 函数:

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

接下去会设置 module 的上下文,因为可能存在 namespaced,需要额外处理。

// 创建上下文环境
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
        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
        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 和 state 必须实时获取,因为他们会被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 => {
    // skip if the target getter is not match this namespace
    if (type.slice(0, splitPos) !== namespace) return

    // extract local getter type
    const localType = type.slice(splitPos)

    // Add a port to the getters proxy.
    // Define as getter property because
    // we do not want to evaluate the getters in this time.
    Object.defineProperty(gettersProxy, localType, {
      get: () => store.getters[type],
      enumerable: true
    })
  })

  return gettersProxy
}

/**
 *  统一对象风格
 */
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 }
}

这里会判断 module 的 namespace 是否存在,不存在不会对 dispatch 和 commit 做处理,如果存在,给 type 加上 namespace,如果声明了{root: true}也不做处理,另外 getters 和 state 需要延迟处理,需要等数据更新后才进行计算,所以使用 Object.defineProperties 的 getter 函数,当访问的时候再进行计算。

再回到上面的流程,接下去是逐步注册 mutation action getter 子 module,先看注册 mutation。

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)
  })
}

mutation 的注册比较简单,主要是包一层函数,然后保存到 store._mutations 里面,在这里也可以知道,mutation 可以重复注册,不会覆盖,当用户调用 this.$store.commit(mutationType, payload)时会触发,接下去看看 commit 函数:

commit (_type, _payload, _options) {
  // 先统一一下参数
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]
  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'
    )
  }
}

首先对参数进行统一处理,因为是支持对象风格和载荷风格的,然后拿到当前 type 对应的 mutation 数组,使用 _withCommit 包裹逐一执行,这样我们执行 this.$store.commit 的时候会调用对应的 mutation,而且第一个参数是 state,然后再执行 mutation 的订阅函数

接下去看 action 的注册

function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    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)
    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
    }
  })
}

和 mutation 很类似,使用函数包一层然后 push 到 store._actions 中,有些不同的是执行时参数比较多,这也是为什么我们在写 action 时可以解构拿到 commit 等的原因,然后再将返回值 promisify,这样可以支持链式调用,但实际上用的时候最好还是自己返回 promise,因为通常 action 是异步的,比较多见是发起 ajax 请求,进行链式调用也是想当异步完成后再执行,具体根据业务需求来。接下去再看看 dispatch 函数的实现:

  dispatch (_type, _payload) {
    // 先统一一下参数
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload)

    const action = { type, payload }
    const entry = this._actions[type]
    if (!entry) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    // 遍历执行订阅者函数,并传入当前设置以执行指定的订阅者
    try {
      this._actionSubscribers
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (process.env.NODE_ENV !== 'production') {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

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

    return result.then(res => {
      try {
        this._actionSubscribers
          .filter(sub => sub.after)
          .forEach(sub => sub.after(action, this.state))
      } catch (e) {
        if (process.env.NODE_ENV !== 'production') {
          console.warn(`[vuex] error in after action subscribers: `)
          console.error(e)
        }
      }
      return res
    })
  }

这里和 commit 也是很类似的,对参数统一处理,拿到 action 数组,如果长度大于一,用 Promise.all 包裹,不过直接执行,然后返回执行结果。

接下去是 getters 的注册和子 module 的注册

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 state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

首先对 getters 进行判断,和 mutation 是不同的,这里是不允许重复定义的,然后包裹一层函数,这样在调用时只需要给上 store 参数,而用户的函数里会包含 local.state local.getters store.state store.getters

// 递归注册子module
installModule(store, rootState, path.concat(key), child, hot)

使用 vue 实例保存 state 和 getter

接着再继续执行 resetStoreVM(this, state),将 state 和 getters 存放到一个 vue 实例中,

// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
resetStoreVM(this, state)

找到函数:

function resetStoreVM (store, state, hot) {
  // 先备份旧有的 vm
  const oldVm = store._vm

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store) // partial 函数转换完相当于 () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // 暂时将Vue设为静默模式,避免报出用户加载的某些插件触发的警告
  const silent = Vue.config.silent
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

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

  // 若不是初始化过程执行的该方法,将旧的组件state设置为null,强制更新所有监听者(watchers),待更新生效,DOM更新完成后,执行vm组件的destroy方法进行销毁,减少内存的占用
  if (oldVm) {
    if (hot) {
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

这里会重新设置一个新的 vue 实例,用来保存 state 和 getter,getters 保存在计算属性中,会给 getters 加一层代理,这样可以通过 this.$store.getters.xxx 访问到,而且在执行 getters 时只传入了 store 参数,这个在上面的 registerGetter 已经做了处理,也是为什么我们的 getters 可以拿到 state getters rootState rootGetters 的原因。

然后根据用户设置开启 strict 模式,使用$watch 来观察 state 的变化,如果此时的 store._committing 不为 true,便是在 mutation 之外修改 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 })
}

最后,如果存在 oldVm,解除对 state 的引用,等 dom 更新后把旧的 vue 实例销毁。

插件注册

再次回到构造函数,接下来是各类插件的注册:

// apply plugins
plugins.forEach(plugin => plugin(this))

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
  devtoolPlugin(this)
}

Vuex 中可以传入 plguins 选项来安装各种插件,这些插件都是函数,接受 store 作为参数,Vuex 中内置了 devtool 和 logger 两个插件:

// devtool.js

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

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  store._devtoolHook = devtoolHook

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

  // 提供“时空穿梭”功能,即state操作的前进和倒退
  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) => {
      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

        // render
        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))

        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')
 */
function pad (num, maxLength) {
  return repeat('0', maxLength - num.toString().length) + num
}

到这里 store 的初始化工作已经完成。

store 其他 api

看到这里,相信已经对 store 的一些实现细节有所了解,另外 store 上还存在一些 api,但是用到的比较少,可以简单看看都有些啥

  • watch (getter, cb, options)

用于监听一个 getter 值的变化

  // 监视数据
  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)
  }

首先判断 getter 必须是函数类型,使用 $watch 方法来监控 getter 的变化,传入 state 和 getters 作为参数,当值变化时会执行 cb 回调。调用此方法返回的函数可停止侦听。

  • replaceState(state)

用于修改 state,主要用于 devtool 插件的时空穿梭功能,代码也相当简单,直接修改_vm.$$state

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

用于动态注册 module:

  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、mutation、action、getter
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // 重置虚拟 store
    resetStoreVM(this, this.state)
  }

首先统一 path 的格式为 Array,接着是断言,path 只接受 String 和 Array 类型,且不能注册根 module,然后调用store._modules.register方法收集 module,也就是上面的 module-collection 里面的方法。再调用 installModule 进行模块的安装,最后调用 resetStoreVM 更新_vm

  • unregisterModule (path)

根据 path 注销动态注册的 module:

  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)
    // 将其state从其父模块上删除
    this._withCommit(() => {
      const parentState = getNestedState(this.state, path.slice(0, -1))
      Vue.delete(parentState, path[path.length - 1])
    })
    // 重置模块,也就是重新安装
    resetStore(this)
  }

和 registerModule 一样,首先统一 path 的格式为 Array,接着是断言,path 只接受 String 和 Array 类型,接着调用 store._modules.unregister 方法注销 module,然后在 store._withCommit 中将该 module 的 state 通过 Vue.delete 移除。最后调用 resetStore 方法,需要再看看 resetStore 的实现

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(store, state, [], store._modules.root, true)
  // 重设 vm
  resetStoreVM(store, state, hot)
}

这里是将_actions _mutations _wrappedGetters _modulesNamespaceMap 都清空,然后调用 installModule 和 resetStoreVM 重新进行全部模块安装和 _vm 的设置

  • hotUpdate (newOptions)

热更新模块。

  hotUpdate (newOptions) {
    // 升级模块,然后重新载入模块
    this._modules.update(newOptions)
    resetStore(this, true)
  }
  • _withCommit (fn)
  // 在 commit 的时候执行,主要是修改 committing 状态,执行回调,修改内容,再将 committing 状态改回去
  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }

在执行 mutation 的时候,会将 _committing 设置为 true,执行完毕后重置,在开启 strict 模式时,会监听 state 的变化,当变化时 _committing 不为 true 时会给出警告

  • subscribe (fn)

将函数加入到 mutation 订阅列表中

  subscribe (fn) {
    return genericSubscribe(fn, this._subscribers)
  }

来看看 genericSubscribe 函数:

// 通用订阅,返回一个函数,这个函数是用于从 subs 中删除插入的 fn 的
function genericSubscribe (fn, subs) {
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}
  • subscribeAction (fn)

将函数加入到 action 的订阅列表中

  subscribeAction (fn) {
    const subs = typeof fn === 'function' ? { before: fn } : fn
    return genericSubscribe(subs, this._actionSubscribers)
  }

辅助函数

为了避免每次都需要通过 this.$store 来调用 api,vuex 提供了 mapState mapMutations mapGetters mapActions createNamespacedHelpers 等 api,接着看看各 api 的具体实现,存放在src/helpers.js

一些工具函数

下面这些工具函数是辅助函数内部会用到的,可以先看看功能和实现,主要做的工作是数据格式的统一和通过 namespace 获取 module。


/**
 * 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 的形式,返回一个数组,方便调用,传入参数只能是数组或者对象
 * normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
 * normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
 */
function normalizeMap (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 += '/'
    }
    return fn(namespace, map)
  }
}

/**
 * 拿到模块名拿到对应的模块
 */
function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (process.env.NODE_ENV !== 'production' && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}

mapState

为组件创建计算属性以返回 store 中的状态

/**
 * Reduce the code which written in Vue.js for getting the state.
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} states # Object's item can be a function which accept state and getters for param, you can do something for state and getters in it.
 * @param {Object}
 */
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
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        state = module.context.state
        getters = module.context.getters
      }
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // 标记在 devtools 中的 vuex key,将其 flag 调为 true
    res[key].vuex = true
  })
  return res
})

mapState 是 normalizeNamespace 的返回值,从上面的代码可以看到 normalizeNamespace 是进行参数处理,如果存在 namespace 便加上命名空间,对传入的 states 进行 normalizeMap 处理,也就是数据格式的统一,然后遍历,对参数里的所有 state 都包裹一层函数,最后返回一个对象

大概是这么回事吧

export default {
  // ...
  computed: {
    ...mapState(['stateA'])
  }
  // ...
}

等价于

export default {
  // ...
  computed: {
    stateA () {
      return this.$store.stateA
    }
  }
  // ...
}

mapGetters

将 store 中的 getter 映射到局部计算属性中

/**
 * Reduce the code which written in Vue.js for getting the getters
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} getters
 * @return {Object}
 */
export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  normalizeMap(getters).forEach(({ key, val }) => {
    // 如果有命名空间,那么会自动加上,如果没有命名空间,会加上 '' ,这样其实没有改变
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && !(val in this.$store.getters)) {
        console.error(`[vuex] unknown getter: ${val}`)
        return
      }
      return this.$store.getters[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

同样的处理方式,遍历 getters,只是这里需要加上命名空间,这是因为在注册时_wrapGetters中的 getters 是有加上命名空间的

mapMutations

创建组件方法提交 mutation

/**
 * Reduce the code which written in Vue.js for committing the mutation
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} mutations # Object's item can be a function which accept `commit` function as the first param, it can accept anthor params. You can commit mutation and do any other things in this function. specially, You need to pass anthor params from the mapped function.
 * @return {Object}
 */
export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  normalizeMap(mutations).forEach(({ key, val }) => {
    res[key] = function mappedMutation (...args) {
      // Get the commit method from store
      let commit = this.$store.commit
      // 处理命名空间的情况,如果存在命名空间,则调整参数
      if (namespace) {
        const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
        if (!module) {
          return
        }
        commit = module.context.commit
      }
      return typeof val === 'function'
        ? val.apply(this, [commit].concat(args))
        : commit.apply(this.$store, [val].concat(args))
    }
  })
  return res
})

和上面都是一样的处理方式,这里在判断是否存在 namespace 后,commit 是不一样的,上面可以知道每个 module 都是保存了上下文的,这里如果存在 namespace 就需要使用那个另外处理的 commit 等信息,另外需要注意的是,这里不需要加上 namespace,这是因为在 module.context.commit 中会进行处理,忘记的可以往上翻,看 makeLocalContext 对 commit 的处理

mapAction

创建组件方法分发 action

/**
 * Reduce the code which written in Vue.js for dispatch the action
 * @param {String} [namespace] - Module's namespace
 * @param {Object|Array} actions # Object's item can be a function which accept `dispatch` function as the first param, it can accept anthor params. You can dispatch action and do any other things in this function. specially, You need to pass anthor params from the mapped function.
 * @return {Object}
 */
export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  normalizeMap(actions).forEach(({ key, val }) => {
    res[key] = function mappedAction (...args) {
      // get dispatch function from store
      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
})

和 mapMutations 基本一样的处理方式

总结

到这里基本 vuex 的流程源码已经分析完毕,分享下自己看源码的思路或者过程,在看之前先把官网的文档再仔细过一遍,然后带着问题来看源码,这样效率会比较高,利用 chrome 在关键点打开 debugger,一步一步执行,看源码的执行过程,数据状态的变换。