Vuex源码分析

# Vuex源码分析

# 前言

在前端项目开发过程当中,Vuex (opens new window)是一个专为 Vue.js (opens new window) 应用程序开发的状态管理工具。

以下是我的Vuex源码学习记录,也是我第一次比较完整的源码阅读。

从学习到整理,爆肝一月有余,成此篇。

# Vuex的使用

这里简单回顾下Vuex的使用,具体可进入官方文档 (opens new window)查询。

1、首先是创建store实例对象:

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

/**
 * 使用Vue.use()方法安装Vuex。
 * Vue.use(plugin):安装插件,如果插件是一个对象,必须提供 install 方法;
 * 如果插件是一个函数,它会被作为 install 方法;install 方法调用时,会将 Vue 作为参数传入。
 * 该方法需要在调用 new Vue() 之前被调用。
 */
Vue.use(Vuex)

// 创建store实例对象,并传入参数
const store = new Vuex.Store({
    strict: process.env.NODE_ENV === 'development', // 判断是否是生产模式,若是则开启严格模式
    state: {...}, // 状态,也就是Vuex核心管理的对象
    getters: {...}, // 派生状态,对state的二次包装
    mutations: {...}, // 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation,必须为同步函数
    actions: {...}, // Action 类似于 mutation,不同在于它可以执行异步操作
    modules: {...} // 传入的子模块,每个模块拥有自己的 state、getter、mutation、action、甚至是嵌套子模块
})

// 将store对象导出
export default store

2、然后,为Vue实例提供创建好的store对象:

// app.js
new Vue({
  el: '#app',
  store, // 传入store
  ...
})

3、组件中进行使用:

methods: {
  increment() {
    this.$store.commit('increment') // 调用store对象中mutations中定义的方法
    console.log(this.$store.state.count) // 获取store对象中state中定义的状态
  }
}

Vuex通过对需要用到的state状态进行集中式的全局管理,使组件间的通信变得简单清晰;

我们使用时也比较简单,在state中初始化状态,在mutationsactions中定义方法用于更改state,在组件中直接调用方法就能更新state,用到该state的组件也会同步更新。

在开始学习源码之前,我先整理了几个有疑惑的问题,以便更有针对性地阅读源码:

问题:

1、vuex中的store是怎么进行全局注入的?
2、vuex是怎么进行状态更改并响应的?
3、this.$store.dispatch({commit, state}, payload)调用时的第一个参数是在哪里传入的?
4、state的更改是如何限制的?

特别说明

以下内容对照源码食用效果更佳,我这里所阅读源码版本为v3.6.2,版本更新会有些微差异,但大致逻辑应该都一样。

# 源码阅读

# 源码目录结构分析

首先从github上clone下来vuex的源码:github地址 (opens new window),然后打开项目,看下源码目录结构:

// 这里主要分析src目录下的文件

│—— src
    │—— module    // 与模块相关的操作
    │   ├── module-collection.js   // 用于收集并注册根模块以及嵌套模块
    │   └── module.js   // 定义Module类,存储模块内的一些信息,例如: state...
    │
    ├── plugins   // 一些插件
    │   ├── devtool.js   // 开发调试插件
    │   └── logger.js    // 
    │
    ├── helpers.js       // 辅助函数,例如:mapState、mapGetters、mapMutations...
    ├── index.cjs.js     // commonjs 打包入口
    ├── index.js         // 入口文件
    ├── index.mjs        // es6 module 打包入口
    ├── mixin.js         // 将vuex实例挂载到全局Vue的$store上
    ├── store.js         // 核心文件,定义了Store类
    └── util.js          // 提供一些工具函数,例如: deepCopy、isPromise、isObject...

# 入口文件index.js

首先找到入口文件index.js,打开:

// index.js

// Store 构造函数和 install 方法
import { Store, install } from './store'
// 辅助函数
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
// 插件
import createLogger from './plugins/logger'

// 默认导出一个对象
export default {
  Store,
  install, // 当执行Vue.use(Vuex)时,会调用install方法,稍后会着重分析这个方法
  version: '__VERSION__', // 版本号
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}
...

回想一下,刚才我们在项目中通过const store = new Vuex.Store({...})来生成store实例对象,可见源码中的Store类应该是包含着Vuex的核心代码,那接下来,我们进入到store.js文件,看看里面的Store到底是怎样定义的~

# store.js

当打开store.js文件的时候,几百行完全看不懂的代码以及全英文的注释确实很容易把人劝退;冷静之后我首先把文件中展开的方法合上,细节逻辑先不看,只是梳理下Store类中代码执行的先后顺序,等到流程大致清晰之后,我再开始挨个阅读各个部分的代码。

整个Store类的主要逻辑其实都在它的constructor函数中,因此我们就从constructor中分步去捋逻辑、看代码。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 










// store.js

// 引入util中方法
import { forEachValue, isObject, isPromise, assert, partial } from './util'
let Vue // 声明Vue变量(会在install方法中赋值)

export class Store {
  // 构造函数
  constructor (options = {}) {
    // 判断是否已安装Vue
    // 若未安装Vue,但Vue已经挂载在window上,自动调用install方法进行安装
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    // 环境判断
    if (__DEV__) { // __DEV__ 是webpack.config.js定义的全局环境变量,有值则为开发环境
      // 断言 
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) // Vue如果未安装,则提示必须先调用Vue.use(Vuex)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`) // 必须提供Promise
      assert(this instanceof Store, `store must be called with the new operator.`) // 必须使用new操作符调用Store函数。
    }
  }
}

# 1、install方法

这里我们先看下install方法里是怎么定义的,这个方法当我们在项目中执行Vue.use(Vuex)时也会调用:

// 安装Vue,初始化vuex
// 当使用 Vue.use() 必须提供一个 install 方法,第一个参数 是Vue 构造器,所以说可以拿到 vue的一切方法和属性
export function install (_Vue) {
  if (Vue && _Vue === Vue) { // 先判断 Vue是否已安装,如果已安装,则直接提示
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue // 如果Vue未安装,则将传入的_Vue赋给Vue,并调用 applyMixin 方法,初始化Vuex;现在移步到 ./mixin.js 文件:
  applyMixin(Vue)
}

打开mixin.js文件:

// mixin.js

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0]) // 首先判断Vue版本号

  if (version >= 2) { // 2.x版本直接通过全局混入Vue.mixin的方式在beforeCreate生命周期里执行vuexInit方法
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // override init and inject vuex init procedure  注入vuex初始化流程
    // for 1.x backwards compatibility.  1.x版本向后兼容
    const _init = Vue.prototype._init // Vue原型上挂载_init方法来进行初始化
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options) // 执行_init()方法
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   * vuex初始化
   * 将vuex混入到$options中
   * 通过 Vue.minxin 方法做了一个全局的混入,在每个组件 beforeCreate 生命周期时会调用 vuexInit 方法,该方法处理得非常巧妙,首先获取当前组件的 $options ,判断当前组件的 $options 上是否有 sotre ,
   * 若有则将 store 赋值给当前组件,即 this.$store ,这个一般是判断根组件的,因为只有在初始化 Vue 实例的时候我们才手动传入了 store ;
   *  若 $options 上没有 store ,则代表当前不是根组件,所以我们就去父组件上获取,并赋值给当前组件,即当前组件也可以通过 this.$store 访问到 store 实例了,
   * 这样一层一层传递下去,实现所有组件都有$store属性,这样我们就可以在任何组件中通过this.$store访问到store
   */

  function vuexInit () {
    // 获取当前组件的 $options,$options为当我们new Vue({...})初始化时传入的参数
    const options = this.$options
    // store injection  store注入
    // 若当前组件的$options上已存在store,一般是root节点,则将$options.store赋值给this.$store(一般是用于根组件的)
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    // 当前组件的$options上没有store,则获取父组件上的$store,即$options.parent.$store,并将其赋值给this.$store(一般用于子组件)
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}

以上就是install方法里Vue安装和Vuex初始化的全部逻辑了,接下来继续回到store.js中的constructor函数中,install之后就是环境判断了:

  • 环境判断















 
 
 
 
 
 
 



// store.js

// 引入util中方法
import { forEachValue, isObject, isPromise, assert, partial } from './util'
let Vue // 声明Vue变量(会在install方法中赋值)

export class Store {
  // 构造函数
  constructor (options = {}) {
    // 判断是否已安装Vue
    // 若未安装Vue,但Vue已经挂载在window上,自动调用install方法进行安装
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    // 环境判断
    if (__DEV__) { // __DEV__ 是webpack.config.js定义的全局环境变量,有值则为开发环境
      // 断言 
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`) // Vue如果未安装,则提示必须先调用Vue.use(Vuex)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`) // 必须提供Promise
      assert(this instanceof Store, `store must be called with the new operator.`) // 必须使用new操作符调用Store函数。
    }
  }
}

这里发现用到了util.js文件中的assert断言方法,它其实原理特别简单,就是判断传入的第一个参数Boolean类型是否为true,若不是就直接抛出第二个参数的提示文案,阻断代码后续执行;

其实在util.js里面还封装了一些其他的工具函数,这里先进入到util.js文件看看里面封装了哪些方法:

  • util.js

进入到util.js文文件里面,对里面定义的工具函数加一下注释,方便之后的理解和使用~

// util.js

/**
 * 查看数组中第一个匹配的元素
 * @param {Array} list
 * @param {Function} f
 * @return {*}
 */
export function find (list, f) {
  return list.filter(f)[0]
}

/**
 * 深拷贝
 * @param {*} obj 传入的复制对象
 * @param {Array<Object>} cache 缓存
 * @return {*}
 */
export function deepCopy (obj, cache = []) {
  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 for object
 * 遍历对象方法封装
 */
export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

/**
 * 
 * @param {*} obj
 * 判断是否为对象,排除null 
 */

export function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

/**
 * 
 * @param {*} val
 * 判断是否为Promise对象 
 */

export function isPromise (val) {
  return val && typeof val.then === 'function'
}

/**
 * 断言
 * @param {*} condition 
 * @param {*} msg 
 */

export function assert (condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}


/**
 * 保留原始参数的闭包函数
 * @param {*} fn 
 * @param {*} arg 
 */
export function partial (fn, arg) {
  return function () {
    return fn(arg)
  }
}

梳理完发现里面定义的方法也不多,大致包含数据类型的判断、断言,及对数据遍历的封装等方法。

梳理完成之后再回到store.js文件,继续刚才我们的流程;等到环境判断完成之后,开始进行内部变量初始化:

# 2、内部变量初始化

import ModuleCollection from './module/module-collection'

const {
      plugins = [],
      strict = false
    } = options // 生成Store类的入参

    // store internal state
    this._committing = false  // 表示提交的状态,当通过mutations方法改变state时,该状态为true,state值改变完后,该状态变为false; 在严格模式下会监听state值的改变,当改变时,_committing为false时,会发出警告,即表明state值的改变不是经过mutations的
    this._actions = Object.create(null)  // // 用于记录所有存在的actions方法名称(包括全局的和命名空间内的,且允许重复定义)
    this._actionSubscribers = []  // 存放actions方法订阅的回调函数
    this._mutations = Object.create(null) // 用于记录所有存在的的mutations方法名称(包括全局的和命名空间内的,且允许重复定义)
    this._wrappedGetters = Object.create(null)  // 收集所有模块包装后的的getters(包括全局的和命名空间内的,但不允许重复定义)
    this._modules = new ModuleCollection(options) // 根据传入的options配置,注册各个模块,此时只是注册、建立好了各个模块的关系,已经定义了各个模块的state状态,但getters、mutations等方法暂未注册
    this._modulesNamespaceMap = Object.create(null) // 存储定义了命名空间的模块
    this._subscribers = []  // 存放mutations方法订阅的回调
    this._watcherVM = new Vue()  // 用于监听state、getters,用于响应式地监测一个 getter 方法的返回值
    this._makeLocalGettersCache = Object.create(null)  // getters的本地缓存

    // bind commit and dispatch to self
    const store = this

这里注意到,在进行_modules变量初始化的时候是直接new一个ModuleCollection实例,传入的是options参数,这个类里面做了什么呢?带着疑问,我打开了./module/module-collection.js文件:

# 3、module的递归收集

// ./module/module-collection.js

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

// ModuleCollection 类的工作就是将保留原来的模块关系,将每个模块封装到一个 Module 类中
// Store中调用:this._modules = new ModuleCollection(options)
export default class ModuleCollection {
  /**
   * rawRootModules为传入的options
   * const options = {
   *    state: {...},
   *    getters: {...},
   *    mutations: {...},
   *    actions: {...},
   *    modules: {
   *        module1: {..., moduleA:{...}},
   *        module2: {..., moduleB:{...}}
   *    }
   * }
   */
  constructor (rawRootModule) {
    // register root module (Vuex.Store options)
    // 注册
    // 前两个参数分别为:[] 、rawRootModule ,此时肯定是从根模块开始注册的,所以 path 里无内容,并且 rawRootModule 指向的是根模块
    this.register([], rawRootModule, false)
  }

  // 根据路径顺序,从根模块开始递归获取到我们准备添加新的模块的父模块
  // 根据传入的 path 路径,获取到我们想要的 Module 类
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key) // 获取子模块,例:['son1','son2','son3'],根据嵌套关系,找到son3模块
    }, this.root)
  }

  // 获取模块的命名空间
  /**
  * 根据模块是否有命名空间来设定一个路径名称
  * 例如:A为父模块,B为子模块,C为子孙模块
  * 1. 若B模块命名空间为second,C模块未设定命名空间时; C模块继承了B模块的命名空间,为 second/
  * 2. 若B模块未设定命名空间, C模块命名空间为third; 则此时B模块继承的是A模块的命名空间,而C模块的命名空间路径为 third/
  * 3. 若B模块和C模块命名分别为second,third;则path传['second', 'third'],返回 second/third/
  */
  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)
  }

  /**
   * 
   * @param {*} path 表示模块嵌套关系, 例:['module1', 'moduleA']
   * @param {*} rawModule 传入的模块对象
   * @param {*} runtime 表示程序运行时
   */
  // 递归注册模块
  register (path, rawModule, runtime = true) {
    if (__DEV__) { // 入参格式判断
      assertRawModule(path, rawModule)
    }

    const newModule = new Module(rawModule, runtime) // 初始化一个新模块
    if (path.length === 0) { // 通过 if(path.length === 0) 判断是否为根模块,是的话就将 this.root 指向 Module
      this.root = newModule
    } else { // 子模块
      // 取父级路径:[1,2,3].slice(0,-1) = [1,2]
      // 
      const parent = this.get(path.slice(0, -1)) // 获取该模块的父模块
      parent.addChild(path[path.length - 1], newModule) // 将该模块添加到它的父模块上
    }

    // register nested modules
    // 有嵌套模块,继续注册
    if (rawModule.modules) {
       /**
       *  1. 遍历所有的子模块,并进行注册;
       *  2. 在path中存储除了根模块以外所有子模块的名称
       * 例:根模块下modules为: {
       *      moduleA:{state:{...}, getters:{...}, mutations:{...}, actions: {...}, modules: {...}},
       *      moduleB:{...}
       * }
       * 执行forEachValue后,会对modules下的每一个模块进行遍历,以moduleA为例,rawChildModule就是moduleA模块的内容,key就是'moduleA',moduleA的注册就是 this.register(['moduleA'], rawModule.modules.moduleA, runtime)
       *  */ 
      forEachValue(rawModule.modules, (rawChildModule, 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) {
      if (__DEV__) {
        console.warn(
          `[vuex] trying to unregister module '${key}', which is ` +
          `not registered`
        )
      }
      return
    }

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

// 更新函数
/**
 * 
 * @param {*} path ['module1', 'moduleA', ...]
 * @param {*} targetModule 根模块
 * @param {*} newModule 当前模块
 */
function update (path, targetModule, newModule) {
  if (__DEV__) {
    assertRawModule(path, newModule) // 判断当前模块参数格式是否正确
  }

  // update target module
  targetModule.update(newModule)

  // update nested modules
  if (newModule.modules) { // 更新嵌入的子模块
    for (const key in newModule.modules) {
      if (!targetModule.getChild(key)) {
        if (__DEV__) {
          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]
      )
    }
  }
}


/**
 * 格式判断
 */
// 判断是否是function
const functionAssert = {
  assert: value => typeof value === 'function',
  expected: 'function'
}

// 判断是否是object
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
}

// 模块格式判断:判断传入的getters,mutations,actions是否格式正确
function assertRawModule (path, rawModule) {
  Object.keys(assertTypes).forEach(key => {
    if (!rawModule[key]) return

    const assertOptions = assertTypes[key]

    // 循环遍历传入的getters,mutations,actions对象
    // 如actions中传入clearToken方法:actions: { clearToken({dispatch, commit}){...} }
    forEachValue(rawModule[key], (value, type) => {
      assert(
        assertOptions.assert(value), // 判断clearToken类型
        makeAssertionMessage(path, key, type, value, assertOptions.expected)
      )
    })
  })
}

// 格式报错提示
/** 
 *  例:path: [], key: 'actions', type: 'clearToken', value: ({dispatch, commit}) {...}, expected: 'function or object with "handler" function'
 * return 'actions should be function or object with "handler" function but "actions.cleaToken" is  "({dispatch, commit}) {...}"'
 * */ 
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
}

这里面代码稍微有点多,不要着急,可以先从constructor开始,看看里执行了什么,发现里面就执行了一个register方法,那就从这个注册方法开始阅读,这里梳理了大致逻辑:

  1. 调用reigister方法注册根模块,通过new Module初始化一个模块,稍后我们看下Module这个类里做了什么;
  2. 判断是否是根模块,是则将当前模块赋值给root;不是则将当前模块添加到它的父模块下;
  3. 如果当前模块有子modules,则遍历所有子模块,递归进行模块注册,重复上面的1、2步流程。

其实这个js里面核心的逻辑就是这个register方法,其他的都是自定义的辅助类方法,如:assertRawModule参数类型判断、unregister卸载,getNamespace获取命名空间等等;接下来我们看一下Module这个类里做了什么:

// module.js

import { forEachValue } from '../util'

// Base data struct for store's module, package with some attribute and method
// 定义了Vuex中的 Module 类,包含了state、mutations、getters、actions、modules
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    // Store some children item
    this._children = Object.create(null)  // 创建一个空对象,用于存放当前模块的子模块
    // Store the origin module object which passed by programmer
    this._rawModule = rawModule // 当前模块的一些信息,例如:state、mutations、getters、actions、modules
    const rawState = rawModule.state

    // Store the origin module's state
    // 函数类型 => 返回一个obj对象; 2. 直接获取到obj对象
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  // 判断该模块是否定义了namespaced,定义了则返回true; 否则返回false
  get namespaced () {
    return !!this._rawModule.namespaced
  }

  // 添加子模块,名称为key
  addChild (key, module) {
    this._children[key] = module
  }

  // 移除名称为key的子模块
  removeChild (key) {
    delete this._children[key]
  }

  // 获取名称为key的子模块
  getChild (key) {
    return this._children[key]
  }

  // 是否存在名称为key的子模块
  hasChild (key) {
    return key in this._children
  }

  // 将当前模块的命名空间更新到指定模块的命名空间中,并同时更新一下actions、mutations、getters的调用来源
  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)
  }

  // 遍历当前模块的所有getters,并执行回调操作
  forEachGetter (fn) {
    if (this._rawModule.getters) {
      forEachValue(this._rawModule.getters, fn)
    }
  }

  // 遍历当前模块的所有actions,并执行回调操作
  forEachAction (fn) {
    if (this._rawModule.actions) {
      forEachValue(this._rawModule.actions, fn)
    }
  }

  // 遍历当前模块的所有mutations,并执行回调操作
  forEachMutation (fn) {
    if (this._rawModule.mutations) {
      forEachValue(this._rawModule.mutations, fn)
    }
  }
}

这里面定义了Vuex的Module类,初始化了state属性,提供了一些模块的操作方法。

都这里,已经大致了解了模块的递归收集原理了,现在我们继续回到store.js上来:

# 4、dispath和commit方法的包装

封装替换原型中的dispatch和commit方法,将this指向当前store对象。





 
 
 
 
 
 
 
 


// bind commit and dispatch to self
    const store = this
    // 将 dispatch 和 commit 方法绑定到 Store 的实例上,避免后续使用dispatch或commit时改变了this指向
    // 这段代码首先对 Store 实例上的 dispatch 和 commit 方法进行了一层包装,即通过 call 将这两个方法的作用对象指向当前的 Store 实例,这样就能防止后续我们操作时,出现 this.$store.dispatch.call(obj, 1) 类似的情况而报错
    const { dispatch, commit } = this
    this.dispatch = function boundDispatch (type, payload) {
      // 这里会将dispath和commit方法的this指针绑定为store
      return dispatch.call(store, type, payload)
    }
    this.commit = function boundCommit (type, payload, options) {
      return commit.call(store, type, payload, options)
    }

dispatch是用来派发actions事件的,如:this.$store.dispatch('addCount', payload)commit是用来执行mutations事件的,如:this.$store.commit('addCount', payload);接下来我们看一下这两个方法是怎么定义的:

// commit
  /**
   * @param {*} _type 事件名称
   * @param {*} _payload 载荷
   * @param {*} _options 参数
   */
  commit (_type, _payload, _options) {
    // check object-style commit
    const {
      type,
      payload,
      options
    } = unifyObjectStyle(_type, _payload, _options) // 整合参数

    // 在处理完参数以后,根据 type 从 store._mutations 上获取到 entry
    const mutation = { type, payload }
    const entry = this._mutations[type] // 查找_mutations上是否有对应的方法
    if (!entry) { // 查找不到则不执行任何操作
      if (__DEV__) {
        console.error(`[vuex] unknown mutation type: ${type}`)
      }
      return
    }
    
    
    // 专用修改state方法,其他修改state方法均是非法修改
    // 在 _withCommit 方法中遍历 entry 依次执行 mutations 方法;
    // 这是因为 Vuex 规定 state 的改变都要通过 mutations 方法,store._committing 这个属性就是用来判断当前是否处于正在调用 mutations 方法
    // 当 state 值改变时,会先去判断 store._committing 是否为 true ,若不为 true ,则表示 state 的值改变没有经过 mutations 方法,于是会打印警告⚠️ 信息
    this._withCommit(() => {
      entry.forEach(function commitIterator (handler) {
        handler(payload)
      })
    })

    // Array.slice() 浅复制,以防止订阅者同步调用unsubscribe时迭代器失效
    this._subscribers // _subscribers 存放mutations的订阅回调
      .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
      .forEach(sub => sub(mutation, this.state))

    if (
      __DEV__ &&
      options && options.silent
    ) {
      console.warn(
        `[vuex] mutation type: ${type}. Silent option has been removed. ` +
        'Use the filter functionality in the vue-devtools'
      )
    }
  }

commit中用到的unifyObjectStyle方法,主要是兼容commitdispatch的两种提交方式:

// unifyObjectStyle
// 整合对象,参数处理
/**
 * 
 * 使用过 Vuex 的应该都知道,commit 有两种提交方式:
 * // 第一种提交方式
      this.$store.commit('func', {num:1})

      // 第二种提交方式
      this.$store.commit({
        type: 'func',
        num: 1
      })
    其先对第一个参数进行判断是否为对象,是的话就当作对象提交风格处理,否则的话就直接返回
 */
function unifyObjectStyle (type, payload, options) {
  if (isObject(type) && type.type) {
    options = payload
    payload = type
    type = type.type
  }

  if (__DEV__) {
    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
  }

  return { type, payload, options }
}

再看下专门修改state的方法_withCommit

// _withCommit
/**  
   * 这个内部api是每次提交状态修改的核心源码,其逻辑很简单,在每次执行状态修改的时候,保证内部属性_committing为true,而这个属性的默认初始值为false。
   * 这样在追踪状态变化当state改变的时候,store._committing如果为false则不是通过_withCommit触发的,一律报错
   */
  _withCommit (fn) {
    const committing = this._committing
    this._committing = true
    fn()
    this._committing = committing
  }

最后我们再看下dispatch里面是如何定义的,其实和commit比较类似:

/** 与上面的commit类似,只是这里是异步函数,需要用Promise作异步处理
   * 通过参数拿到对应注册的actions,然后promise.all执行回调,回调里则是进行commit提交。
   */
  dispatch (_type, _payload) {
    // check object-style dispatch
    const {
      type,
      payload
    } = unifyObjectStyle(_type, _payload) // 整合参数

    const action = { type, payload }
    const entry = this._actions[type] // 查找_actions上是否有对应的方法
    if (!entry) {
      if (__DEV__) {
        console.error(`[vuex] unknown action type: ${type}`)
      }
      return
    }

    try {
      this._actionSubscribers // _actionSubscribers 存放 actions 的订阅回调
        .slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
        .filter(sub => sub.before)
        .forEach(sub => sub.before(action, this.state))
    } catch (e) {
      if (__DEV__) {
        console.warn(`[vuex] error in before action subscribers: `)
        console.error(e)
      }
    }

    // 其中变量 result ,先判断 entry 的长度,若大于1,则表示有多个异步方法,所以用 Promise.all 进行包裹 ; 否则直接执行 entry[0]
    const result = entry.length > 1
      ? Promise.all(entry.map(handler => handler(payload)))
      : entry[0](payload)

    // 返回Promise异步函数
    return new Promise((resolve, reject) => {
      result.then(res => {
        try {
          this._actionSubscribers
            .filter(sub => sub.after)
            .forEach(sub => sub.after(action, this.state))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in after action subscribers: `)
            console.error(e)
          }
        }
        resolve(res)
      }, error => {
        try {
          this._actionSubscribers
            .filter(sub => sub.error)
            .forEach(sub => sub.error(action, this.state, error))
        } catch (e) {
          if (__DEV__) {
            console.warn(`[vuex] error in error action subscribers: `)
            console.error(e)
          }
        }
        reject(error)
      })
    })
  }

关于commit和dispatch里面订阅回调的部分,接触得不多,暂不深入,也不影响对核心源码的理解。

接着继续分析下面的流程,模块初始化已经完成,接下来,就开始比较重要的一步逻辑:每一个模块的信息注册

# 5、注册模块

    // strict mode
    // 判断store是否为严格模式。true: 所有的state都必须经过mutations来改变
    // this.strict 是用于判断是否是严格模式。因为 vuex 中,建议所有的 state 变量的变化都必须经过 mutations 方法,因为这样才能被 devtool 所记录下来,所以在严格模式下,未经过 mutations 而直接改变了 state 的值,开发环境下会发出警告⚠️
    this.strict = strict

    // 将根模块的state赋值给state变量
    const state = this._modules.root.state
    // 1. 从根模块开始,递归注册各个模块的信息
    installModule(this, state, [], this._modules.root)

接下来看一下installModule方法怎么定义的:

// installModule

/**
 * 注册完善各个模块内的信息
 * @param {*} store store实例对象
 * @param {*} rootState state属性
 * @param {*} path 模块路径
 * @param {*} module 模块
 * @param {*} hot 
 */
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length // 是否为根模块

  /**
   * const namespace = store._modules.getNamespace(path) 是将路径 path 作为参数, 调用 ModuleCollection 类实例上的 getNamespace 方法来获取当前注册对象的命名空间的
   * 获取当前模块的命名空间,path传入['second', 'third'],返回 second/third/
   */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  // 如果当前模块设置了namespaced 或 继承了父模块的namespaced,则在modulesNamespaceMap中存储一下当前模块
  if (module.namespaced) {
    if (store._modulesNamespaceMap[namespace] && __DEV__) { // 重复校验
      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
    }
    store._modulesNamespaceMap[namespace] = module // 存储当前模块
  }

  // set state
  // 如果不是根模块,将当前模块的state注册到其父模块的state上
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取当前path父模块的state
    const moduleName = path[path.length - 1] // 当前模块名称
    // 更改state
    store._withCommit(() => {
      if (__DEV__) {
        if (moduleName in parentState) { // 如果父模块中已经存在当前模块名称,则报错提示
          console.warn(
            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
          )
        }
      }
    
      /**
       * Vue.set: 向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。
       * 将当前模块的state注册在父模块的state上,并且是响应式的
       * 调用了 Vue 的 set 方法将当前模块的 state 响应式地添加到了父模块的 state 上,
       * 这是因为在之后我们会看到 state 会被放到一个新的 Vue 实例的 data 中,所以这里不得不使用 Vue 的 set 方法来响应式地添加
       */
      Vue.set(parentState, moduleName, module.state)
    })
  }

  /**
   * 设置当前模块的上下文context
   * 这行代码也可以说是非常核心的一段代码了,它根据命名空间为每个模块创建了一个属于该模块调用的上下文,并将该上下文赋值了给了该模块的 context 属性
   */
  const local = module.context = makeLocalContext(store, namespace, path)

  // 遍历,注册所有mutations
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key  // 例如:first/second/mutations1
    registerMutation(store, namespacedType, mutation, local)
  })

  // 注册模块的所有actions
  module.forEachAction((action, key) => {

     /**
     * actions有两种写法:
     * 
     * actions: {
     *    AsyncAdd (context, payload) {...},   // 第一种写法
     *    AsyncDelete: {                       // 第二种写法
     *      root: true,
     *      handler: (context, payload) {...}
     *    } 
     * }
     */

    const type = action.root ? key : namespace + key  // 判断是否需要在命名空间里注册一个全局的action
    const handler = action.handler || action // 获取actions对应的函数
    registerAction(store, type, handler, local)
  })

  // 注册模块的所有getters
  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)
  })
}

这里面的代码稍微有点多,我们按照从上往下的流程慢慢梳理,具体逻辑如下:

  1. 首先判断是否是根模块,获取当前模块的namespace,并在_modulesNamespaceMap中存储当前模块;
  2. 然后对非根模块进行state注册:通过Vue.set()方法将当前模块的state注册到其父模块的state上;
  3. 接着是执行const local = module.context = makeLocalContext(store, namespace, path)生成模块调用上下文,它根据命名空间为每个模块创建了一个属于该模块调用的上下文,并将该上下文赋值了给了该模块的context属性,local这个变量存储的就是一个模块的上下文,稍后具体看下这个方法怎么实现的;
  4. 模块上下文创建完成后,接下来就是通过遍历注册模块所有的mutations 、actions 、getters了;
  5. 等到mutations 、actions 、getters注册完成,最后就开始遍历所有的子模块,重新调一遍installModule,实现对子模块的递归注册。

以上,就是installModule这个方法的大致处理逻辑,那接下来对刚才有疑惑的地方再具体展开一下,首先是步骤3中的makeLocalContext

  • 生成模块上下文、生成本地缓存
/**
 * make localized dispatch, commit, getters and state
 * if there is no namespace, just use root ones
 * 获取上下文
 */
function makeLocalContext (store, namespace, path) {
  const noNamespace = namespace === '' // 是否设置了命名空间

  // local 这个变量存储的就是一个模块的上下文,若设置了命名空间则创建一个本地的commit、dispatch方法,否则将使用全局的store
  const local = {
    /**
     * 先来看其第一个属性 dispatch ,当该模块没有设置命名空间时,调用该上下文的 dispatch 方法时会直接调用 sotre.dispatch ,即调用了根模块的 dispatch 方法 ; 
     * 而存在命名空间时,会先判断相应的命名空间,以此来决定调用哪个 dispatch 方法
     */
    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options) // 整合入参,兼容传值方式
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) { // 判断调用 dispatch 方法时有没有传入第三个参数 {root: true} ,若有则表示调用全局根模块上对应的的 dispatch 方法
        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) { // 若传入了第三个参数设置了root:true,则派发的是全局上对应的的mutations方法
        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)
    }
  }

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
   /**
   * 然后最后通过 Object.defineProperties 方法对 local 的 getters 属性和 state 属性设置了一层获取代理,等后续对其访问时,才会进行处理。
   * 例如,访问 getters 属性时,先判断是否存在命名空间,若没有,则直接返回 store.getters ; 否则的话,根据命名空间创建一个本地的 getters 缓存,根据这个缓存来获取对应的 getters
   * 如:this.$store.
   */
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  /**
 * 
 *当存在命名空间时访问 local.getters ,首先会去 store._makeLocalGettersCache 查找是否有对应的 getters 缓存;
 * 若无命令空间,则创建一个 gettersProxy ,在 store.getters 上找到对应的 getters ,然后用 Object.defineProperty 对 gettersProxy 做一层处理;
 * 即当访问 local.getters.func 时,相当于访问了 store.getters['first/func'] ,这样做一层缓存,下一次访问该 getters 时,就不会重新遍历 store.getters 了 ; 若有缓存,则直接从缓存中获取
 */

  return local
}

// 创建本地的getters缓存
function makeLocalGetters (store, namespace) {
  // 若缓存中没有指定的getters,则创建一个新的getters缓存到__makeLocalGettersCache中
  if (!store._makeLocalGettersCache[namespace]) {
    const gettersProxy = {}
    const splitPos = namespace.length
    Object.keys(store.getters).forEach(type => {
      // skip if the target getter is not match this namespace
      // 如果store.getters中没有与namespace匹配的getters,则不进行任何操作
      if (type.slice(0, splitPos) !== namespace) return

      // extract local getter type
      // 获取本地getters名称 ?
      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.
      // 对getters添加一层代理
      Object.defineProperty(gettersProxy, localType, {
        get: () => store.getters[type],
        enumerable: true
      })
    })

    // 把代理过的getters缓存到本地
    store._makeLocalGettersCache[namespace] = gettersProxy
  }

  return store._makeLocalGettersCache[namespace]
}

之后便是步骤4中对模块的mutations 、actions 、getters进行注册:

// 注册mutation
function registerMutation (store, type, handler, local) {
  // 首先根据我们传入的 type 也就是上面的 namespacedType 去 store._mutations 寻找是否有入口 entry ,若有则直接获取 ; 否则就创建一个空数组用于存储 mutations 方法
  const entry = store._mutations[type] || (store._mutations[type] = [])
  // 在获取到 entry 以后,将当前的 mutations 方法添加到 entry 末尾进行存储。其中 mutations 接收的参数有两个,即 上下文中的 state 和 我们传入的参数 payload
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload) // store是this指向,
  })

  /**
   * 从这段代码我们可以看出,整个 store 实例的所有 mutations 方法都是存储在 store._mutations 中的,并且是以键值对的形式存放的,例如:
   * store._mutations = {
        'mutations1': [function handler() {...}],
        'ModuleA/mutations2': [function handler() {...}, function handler() {...}],
        'ModuleA/ModuleB/mutations2': [function handler() {...}]
      }
   */
}


// 注册action
/**
 * 
 * 与 mutations 类似,先从 store._actions 获取入口 entry ,然后将当前的 actions 进行包装处理后添加到 entry 的末尾。 
 * actions 方法接收两个参数,即 context 和我们传入的参数 payload ,其中 context 是一个对象,里面包含了 dispatch 、commit 、getters 、state 、rootGetters 、rootState ,
 * 前4个都是在当前模块的上下文中调用的,后2个是在全局上调用的
 */
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []) // 通过store._actions 记录所有注册的actions
  // 接收两个参数:context(包含了上下文中的dispatch方法、commit方法、getters方法、state)、传入的参数payload
  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)

    /**
     * 最后对于 actions 的返回值还做了一层处理,因为 actions 规定是处理异步任务的,所以我们肯定希望其值是一个 promise 对象,这样方便后续的操作。
     * 所以这里对 actions 方法的返回值做了一个判断,如果本身就是 promise 对象,那么就直接返回 ;
     * 若不是,则包装一层 promise 对象,并将返回值 res 作为参数返回给 .then
     */
    if (!isPromise(res)) {  // 若返回值不是一个promise对象,则包装一层promise,并将返回值作为then的参数
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}

// 注册getter
/**
 * 这里发现 getters 并不像 mutations 和 actions 一样去获取一个 entry ,而是直接查看 store._wrappedGetters[type] 是否有对应的 getters ,若有,则不再重复记录 ; 
 * 否则将 getters 包装一下存在 sotre._wrappedGetters 中,其中经过包装后的 getters 接收4个参数,即 state 、getters 、rootState 、rootGetters ,
 * 前2个分别表示当前上下文中的 state 和 getters ,后2个分别表示根模块的 state 和 getters
 */
function registerGetter (store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) { // 若记录过getters了,则不再重复记录
    if (__DEV__) {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }

  // 在store._wrappedGetters中记录getters
  // getters 是不能重名的,并且前一个命名的不会被后一个命名的所覆盖
  store._wrappedGetters[type] = function wrappedGetter (store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}

以上,就是对installModule方法涉及到的逻辑进行的大致梳理,这应该算是vuex里面比较核心的一段逻辑,里面肯定还有一些值得深究的地方,由于篇幅原因这里不做过多阐述了。

模块注册完成之后,接下来Vuex又做了什么呢?我们继续看源码:

    // 注册vm
    resetStoreVM(this, state)

# 6、注册vm

进入到这个方法,看看里面执行了什么:

// 初始化vm
/**
 * 核心内容是store._vm这样一个内部变量,本质上将注册后的state和getters作为新的数据源实例化一个Vue对象传递给store._vm,并且删除旧的store._vm;
 * 与此同时,定义store.getters.xxx=store._vm[xxx],从而完成使用getters的正确姿势。
 * state的使用是由store内部提供了一个api,调用这个api返回store._vm.data.$$state.xxx,在更新store._vm之后,就可以访问这个模块的state。
 */
function resetStoreVM (store, state, hot) {
  const oldVm = store._vm

  // bind store public getters
  store.getters = {} // 在实例store上设置getters对象
  // reset local getters cache
  store._makeLocalGettersCache = Object.create(null) // 清空本地缓存
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  // 循环所有处理过的getters,并新建computed对象进行存储,通过Object.defineProperty方法为getters对象建立属性,使得我们通过this.$store.getters.xxxgetter能够访问到该getters
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // direct inline function use will lead to closure preserving oldVm.
    // using partial to return function with only arguments preserved in closure environment.
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  // 暂时将Vue设为静默模式,避免报出用户加载的某些插件触发的警告
  Vue.config.silent = true
  /**
   * 这个方法里主要做的就是生成一个 Vue 的实例 _vm ,然后将 store._wrappedGetters 里的 getters 以及 store.state 交给一个 _vm 托管,即将 store.state 赋值给 _vm.data.$$state ;
   * 将store._wrappedGetters 通过转化后赋值给 _vm.computed ,这样一来,state 就实现了响应式,getters 实现了类似 computed 的功能
   */
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  // 开启严格模式
  if (store.strict) {
    enableStrictMode(store)
  }

  // 因为生成了新的 _vm ,所以最后通过 oldVm.$destory() 将旧的 _vm 给销毁掉了
  // 值得注意的是,其将 sotre.getters 的操作放在了这个方法里,是因为我们后续访问某个 getters 时,访问的其实是 _vm.computed 中的内容。
  // 因此,通过 Object.defineProperty 对 store.getters 进行了处理
  // 若不是初始化过程执行的该方法,将旧的组件state设置为null,强制更新所有监听者(watchers),待更新生效,DOM更新完成后,执行vm组件的destroy方法进行销毁,减少内存的占用
  if (oldVm) {
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      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模式,如果存在oldVm,解除对state的引用,等dom更新后把旧的vue实例销毁。

里面在严格模式下会执行一个enableStrictMode方法,看看具体做了什么:

// 开启严格模式:监听state的改变
// 当state改变的时候,store._committing如果为false则不是通过_withCommit触发的,一律报错
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

这个比较逻辑简单,就是通过Vue$watch方法监听this._data.$$state的改变,如果有值改变时store._committing不为true,则说明不是通过commit进行更改的,一律报错。

其实到这里,Vuex核心部门的逻辑就已经差不多,我们已经可以通过store.getter.某个getters 来使用getters了,mutationsactions的访问其实在dispath和commit方法的包装那小节就已经讲过,那我们再看下是怎么访问state的:

    // 定义了一个 get 函数,访问state,可以很清楚地看到,当我们访问 store.state 时,就是去访问 store._vm.data.$$state
  get state () {
    return this._vm._data.$$state
  }

  // 如果直接通过this.$store.state.a = 'xxx' 更改state则直接报错,只能通过commit更改
  set state (v) {
    if (__DEV__) {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }

# 7、插件的使用

如果我们传入自定义的插件,可通过下面的代码实现插件的调用:

    // 插件的注入
    // 首先就是遍历创建 Store 类时传入的参数 Plugins ,依次调用传入的插件函数(比如我们常用的用来固化数据的 persistedState 插件,就是这里注入的)
    plugins.forEach(plugin => plugin(this))
    
    const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
    if (useDevtools) {
      // 然后就是调用 devtoolPlugin 方法啦,根据导入的路径我们去到相应的文件
      devtoolPlugin(this)
    }

看一下devtoolPlugin里面都执行了啥:

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

  // 1. 触发Vuex组件初始化的hook
  devtoolHook.emit('vuex:init', store)

  // 2. 提供“时空穿梭”功能,即state操作的前进和倒退
  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

  // 3. mutation被执行时,触发hook,并提供被触发的mutation函数和当前的state状态
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  }, { prepend: true })

  store.subscribeAction((action, state) => {
    devtoolHook.emit('vuex:action', action, state)
  }, { prepend: true })
}

大致就是方便开发时调试用的插件;Vuex还内置了一个logger插件,可以生成状态快照,有兴趣的可以自行看源码,这里不阐述了。

其实到这里Store类里面constructor里面执行的逻辑也梳理的差不多了,其他就是提供的一些方便使用的api和辅助函数。

# 其他方法

# 1. 一些工具函数

  • mutationsactions的订阅函数
// mutations订阅
subscribe (fn, options) {
    return genericSubscribe(fn, this._subscribers, options)
  }

// actions订阅
  subscribeAction (fn, options) {
    const subs = typeof fn === 'function' ? { before: fn } : fn
    return genericSubscribe(subs, this._actionSubscribers, options)
  }
  • watch监听函数
// 监听 getter值的变化
  watch (getter, cb, options) {
    if (__DEV__) {
      assert(typeof getter === 'function', `store.watch only accepts a function.`)
    }
    return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
  }
  • 注册模块、卸载模块
// 注册模块
  registerModule (path, rawModule, options = {}) {
    if (typeof path === 'string') path = [path]

    if (__DEV__) {
      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)
    installModule(this, this.state, path, this._modules.get(path), options.preserveState)
    // reset store to update getters...
    resetStoreVM(this, this.state)
  }

  // 卸载模块
  unregisterModule (path) {
    if (typeof path === 'string') path = [path]

    if (__DEV__) {
      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)
  }

这里简单例举了几个,想看更多的可以自己去看源码。

# 2. 辅助函数

其实我们在使用Vuex的时候经常会用到如:mapState、mapActions这种辅助函数,看一下它们是在哪里定义的:

// index.js
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'

找到helpers.js文件,这里以mapState函数为例:

// 这里的 namespace 是一个字符串,states 是我们刚才处理好的映射变量 map
export const mapState = normalizeNamespace((namespace, states) => {
  // 首先创建一个空对象 res ,这是我们最后处理好要返回的变量;
  const res = {}
  // 然后通过 isValidMap 方法判断 map 是否符合要求,即是否是数组或对象 ;
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  // 标准化states以后对其进行 forEach 遍历,将遍历到的每一个对象经过处理后存放在 res 中,
  normalizeMap(states).forEach(({ key, val }) => {
    res[key] = function mappedState () {
      // 获取根模块的 state 、getters
      let state = this.$store.state
      let getters = this.$store.getters
      if (namespace) {
        // 调用 getModuleByNamespace 方法获取到 namespace 对应的模块 module
        const module = getModuleByNamespace(this.$store, 'mapState', namespace)
        if (!module) {
          return
        }
        // 然后将刚才声明的变量 state 和 getters 替换成 module 对应上下文中的 state 和 getters
        state = module.context.state
        getters = module.context.getters
      }
      /**这里还做了一层处理是因为要处理两种不同的方式,例如:
       * mapState({
          foo: state => state.foo,
          bar: 'bar'
        })
       */
      return typeof val === 'function'
        ? val.call(this, state, getters)
        : state[val]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})
// 标准化命名空间,对入参做兼容处理
function normalizeNamespace (fn) {
  return (namespace, map) => {
    if (typeof namespace !== 'string') {
      map = namespace
      namespace = ''
    } else if (namespace.charAt(namespace.length - 1) !== '/') {
      namespace += '/'
    }
    return fn(namespace, map)
  }
}

// 判断是否是Map:即是否是数组或对象
function isValidMap (map) {
  return Array.isArray(map) || isObject(map)
}

// 标准化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) {
  if (!isValidMap(map)) {
    return []
  }
  return Array.isArray(map)
    ? map.map(key => ({ key, val: key }))
    : Object.keys(map).map(key => ({ key, val: map[key] }))
}

// 通过namespace获取模块
function getModuleByNamespace (store, helper, namespace) {
  // 可以看到 store._modulesNamespaceMap 终于派上了用场,在生成 Store 实例注册所有模块的时候,将带有命名空间的模块都存储在了该变量上,原来是在这里用上了
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}

其他的mapMutations、mapGetters、mapActionsmapState方法类似,这里再看下另一个常用的辅助函数createNamespacedHelpers

// createNamespacedHelpers
export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})

// 该方法是根据传入的命名空间 namespace 创建一组辅助函数,巧妙之处就是先通过 bind 函数把第一个参数先传入
// 以下为项目中使用的例子:

import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('first/second')

export default {
  computed: {
    ...mapState({
      a: 'a',  // 相当于 first/second/a
      b: 'b',  // 相当于 first/second/b
    })
  },
  methods: {
    ...mapActions([
      'foo',      // 相当于 first/second/foo
      'bar',      // 相当于 first/second/bar
    ])
  }
}

看到这里,其实Vuex的源码学习就已经结束了!!

# 流程回顾及问题解答

流程回顾

最后我们来梳理一下Vuex的源码主要做了什么:

  1. 首先肯定是要定义一个install方法,因为我们是通过Vue.use(Vuex)进行安装,那这个方法具体都做了什么呢?我们回顾下源码发现:先是初始化全局变量Vue,之后获取这个传入的Vue$options参数里的store,最后是通过一层层往上查询的方式实现所有组件都挂载了store,这样我们就能在所有组件中通过this.$store获取生成的Store对象了;

这个方法其实也就解决了我在阅读源码之前的第一个问题:vuex中的store是怎么进行全局注入的?

Vue.mixin({ beforeCreate: vuexInit }) // 每个组件进行混入

function vuexInit () {
    // 获取当前组件的 $options,$options为当我们new Vue({...})初始化时传入的参数
    const options = this.$options
    // store injection
    // 若当前组件的$options上已存在store,一般是root节点,则将$options.store赋值给this.$store(一般是用于根组件的)
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    // 当前组件的$options上没有store,则获取父组件上的$store,即$options.parent.$store,并将其赋值给this.$store(一般用于子组件)
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
  1. 之后就是Store实例的初始化:首先是初始化一些内部变量,然后生成ModuleCollection实例_module;之后就是调用installModule方法,通过递归注册所有模块;

在installModule方法中做的内容有很多,主要是:实现子模块state的注册、生成当前模块的context属性,对当前模块的state、getters设置代理,并缓存getters;最后遍历注册所有的mutations、actions、getters,最后递归注册子模块,重复一遍。

在遍历注册mutations、actions、getters,发现会传入当前store作为参数,比如如下actions的注册,这也解决了我的第三个问题:this.$store.dispatch({commit, 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, // 第一个参数为this指向
    { // 第二个参数为自定义的对象,这也是为什么我们在使用时可以这样用:this.$store.dispatch({commit, state}, payload)
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload) // 第三个参数,传入的载荷
}
  1. 之后就是调用resetStoreVM方法注册vm:对所有已经注册的getters、state设置代理监听,通过给store生成一个Vue实例_vm,来实现数据变化的动态更新,其实说白了还是用了Vue的双向数据绑定来实现数据的响应更新,这也解决了我们刚开始的第二个问题:vuex是怎么进行状态更改并响应的?

具体代码如下:

function resetStoreVM (store, state, hot) {
  const oldVm = store._vm
  store.getters = {} // 在实例store上设置getters对象
  store._makeLocalGettersCache = Object.create(null) // 清空本地缓存
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 循环所有处理过的getters,并新建computed对象进行存储,通过Object.defineProperty方法为getters对象建立属性,使得我们通过this.$store.getters.xxxgetter能够访问到该getters
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true
    })
  })

  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
}

之后我们想通过this.$store.state.xxx访问到数据还需定义这样一个方法:

get state () {
    return this._vm._data.$$state // 这样就能访问到绑定在_vm下的数据
  }
  1. 到这里,其实主要逻辑就实现的差不多了,之后就是插件的注入,辅助函数的定义...

现在回头看看还有哪些问题没有解决:

state的更改是如何限制的?

答:这里其实是通过定义了一个变量_committing来限制state的更改,当执行commit方法时,会调用一个_withCommit方法将_committing会更改为true,等到commit操作完成再将_committing设回初始值,这样在严格模式下监听state的更新,只要当state变化,且_committing不为true,就意味着是非法更新,就直接报错,从而限制state的更改。

_withCommit (fn) {
    const committing = this._committing // 这个变量其实在刚才内部变量初始化的时候赋值为 false
    this._committing = true
    fn()
    this._committing = committing
  }

  // 开启严格模式:监听state的改变
// 当state改变的时候,store._committing如果为false则不是通过_withCommit触发的,一律报错
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (__DEV__) {
      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

# 手写mini-Vuex

现在我们也算是对Vuex的源码有了一个完整的梳理,那么接下来我们试着手写一个mini版的Vuex,活学活用一下。

这里我们主要实现Vuex的以下两个核心逻辑:

  • 数据的全局注入,state里面定义的数据在任意组件都能获取到;
  • 状态的统一管理,实现state、getters数据的响应式更新;

这里只是一个极简版的实现,直接上代码了~

// my-vuex-mini.js
let Vue;

// install
function install(_Vue) {
    if (Vue !== _Vue) Vue = _Vue; // 防止重复注册
    Vue.mixin({ // 混入,通过这样在每一个Vue组件中进行混入,就能让每一个组件都能访问到this.$store
        beforeCreate() {
            let opts = this.$options;
            // console.log('====$options',opts);
            if (opts.store) { // 根组件
                this.$store = opts.store; // 兼容函数类型
            } else { // 是子组件,则王上一级父组件找store
                this.$store = opts.parent && opts.parent.$store;
            }
        }
    });
}

// Store
class Store {
    constructor(options = {}) {
        const {
            state = {},
            getters = {},
            mutations = {},
            actions = {}
        } = options;

        // 绑定state
        this._vm = new Vue({
            data() {
                return {
                    $$state: state 
                };
            }
        });

        // 绑定getters
        this.getters = {};
        Object.keys(getters).forEach(key => {
            Object.defineProperty(this.getters, key, { // 响应式绑定getters
                get: () => getters[key](this.state)
            });
        });

        // 定义commit
        this._mutations = mutations;
        this.commit = (type, payload) => {
            this._mutations[type](this.state, payload); // 传入state
        };

        // 定义dispatch
        this._actions = actions;
        this.dispatch = (type, payload) => {
            this._actions[type](this, payload); // 传入当前store
        };
    }

    // 定义state
    get state() {
        return this._vm && this._vm.$data.$$state;
    }
}

export default {
    install,
    Store
};

使用:

// index.js

import Vue from 'vue';
import Vuex from './my-vuex-mini';
Vue.use(Vuex);
const store = new Vuex.Store({
    state: {
        level: 'root',
        num: 1,
    },
    getters: {
        doubleCount: (state, getters) => {
            return state.num * 2;
        }
    },
    mutations: {
        changeNum(state, payload) {
            state.num = payload.num;
        },
    },
    actions: {
        async changeFn({state, commit, dispatch}, payload) {
            changeNum('updateName', payload);
        },
    },
});
var app = new Vue({
    store,
    el: '#app',
    render: h => h(App)
});

// 最后我们在任意组件中都能通过this.$store.state.xxx访问数据;
// 通过this.$store.dispatch('changeFn',{num: xxx})也能派发事件,实现num的动态响应更新

这里暂时未考虑模块的注册,依赖收集等逻辑,这部分我已经完善在我github (opens new window)里了,有兴趣的可以点击进行查看。

# 后记

# 遗留问题

在源码阅读的过程中遇到一些问题,由于时间等因素只能暂时先放在一边,这里先记录下来,等以后有时间再做梳理。

  1. commitdispatch中的订阅回调函数具体是怎么用的?
  2. 模块注册中在生成context上下文时,有些细节还需要再次阅读熟悉
  3. _wrappedGetters中getters的注册及本地缓存是怎么处理的?
  4. 后续完善mini-vuex中的根模块与子模块的注册功能(已完成)。
  5. vuex设计上有没有什么缺陷?可参考下这篇文章:从源码解读 Vuex 的一些缺陷 (opens new window)

# 其他收获

在阅读源码的过程中,发现一些值得学习的写法:

  1. Array.reduce((pre, cur) => return fn()) 累加器的使用;
  2. Vue.mixin混入实现store所有组件的注册
  3. 闭包函数的运用
  4. 递归实现模块的注册

# 参考

上次更新: 1/24/2022, 5:38:42 PM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>