专栏首页前端印象从未看过源码,到底该如何入手?分享一次完整的源码阅读过程

从未看过源码,到底该如何入手?分享一次完整的源码阅读过程

前言

我觉得每个人可能都有过看源码的想法吧,也包括我。因为看源码不光能使自己对这个库更加熟悉,还能学习到作者强大的思想,久而久之,自己的水平和思想也会有明显的提升的。

但对于我来说,之前从来没有阅读过源码,想阅读源码却不敢迈出那一步,因为一个成熟的库有着太多的方法、逻辑,阅读起来可能会比较困难,但人总要勇于尝试的嘛,于是我就准备把 Vuex 的源码 clone 下来,没有别的原因,只是因为这个库体积比较小,算上注释,核心代码只有1000行不到,我觉得非常适合第一次阅读源码的人拿来练手

说干就干,我就先在 github 上给自己列了一个计划表,预计 15 天看完源码并完成总结,然后每天记录一下当天的收获

不过最后的结果倒是出乎我的意料,阅读源码加上整理总结只用了8天左右的时间

在阅读源码之前,我是先去看了一下 Vuex 的官方文档,算是一种回顾、查漏补缺,我也非常建议这样做,因为你看源码,你就会看到这个库里面所有的内容,那么你连这个库都没用明白呢,阅读源码的难度无形之中又增加了嘛!即先会熟练使用这个库的各个方法(尽管你并不知道为何这么使用),再在阅读源码的过程中看到相应的代码时联想到那个方法的使用,两者相互结合,对于源码的理解就变得容易许多了

这里放上 Vuex 官方文档的链接,如果有兴趣跟着我的思路阅读 Vuex 源码的小伙伴可以先把文档中提到的所有使用都熟悉一下 ➡️ Vuex官方文档

文末有 总结问答环节

源码解析

对于源码的所有注释和理解我都收录在我 githubVuex-Analysis 仓库里了,想要看更详细的注释的,可以 fork 下来参考一下 ➡️ Vuex源码解析仓库地址链接(觉得不错的可以点个 star 支持一下)

接下来本文就按照我当时阅读源码的思路,一步一步详细地讲解,希望大家耐心看完,谢谢啦~

一、源码目录结构分析

整个 Vuex 的源码文件非常多,我们直接看最主要的文件,即 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...

二、源码阅读

1. 查看工具函数

首先我个人觉得肯定是要看一下 util.js ,这里面存放的是源码中频繁用到的工具函数,所以我觉得要最先了解一下每个函数的作用是什么

/**
 * Get the first item that pass the test
 * by second argument function
 *
 * @param {Array} list
 * @param {Function} f
 * @return {*}
 */

// 找到数组list中第一个符合要求的元素
export function find (list, f) {
  return list.filter(f)[0]
}

/**
 * 深拷贝
 * 
 * @param {*} obj
 * @param {Array} cache
 * @return {*}
 */
export function deepCopy (obj, cache = []) {
  // just return if obj is immutable value
  if (obj === null || typeof obj !== 'object') {
    return obj
  }

  // if obj is hit, it is in circular structure
  const hit = find(cache, c => c.original === obj)
  if (hit) {
    return hit.copy
  }

  const copy = Array.isArray(obj) ? [] : {}
  // put the copy into cache at first
  // because we want to refer it in recursive deepCopy
  cache.push({
    original: obj,
    copy
  })

  Object.keys(obj).forEach(key => {
    copy[key] = deepCopy(obj[key], cache)
  })

  return copy
}

// 遍历obj对象的每个属性的值
export function forEachValue (obj, fn) {
  Object.keys(obj).forEach(key => fn(obj[key], key))
}

// 判断是否为对象(排除null)
export function isObject (obj) {
  return obj !== null && typeof obj === 'object'
}

// 判断是否为Promise对象
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)
  }
}
 
每个函数的作用我都写上了注释,稍微阅读一下应该可以明白其作用 

2. 入口文件

最主要的代码都在 src 目录下,所以以下提到的文件都是默认 src 目录下的文件 首先,肯定从入口文件 index.js 开始看,但能发现的是,还有 index.cjs 和 index.mjs ,这两者分别是 commonjs 和 es6 module 的打包入口,我们就不用管了

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

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

export {
  Store,
  install,
  mapState,
  mapMutations,
  mapGetters,
  mapActions,
  createNamespacedHelpers,
  createLogger
}
 

从入口文件中可以看到,主要导出了 Store 类 、install 方法以及一些辅助函数(mapState、mapMutations、mapGetters…) 那么我们主要看的就是 vuex 的核心代码,即 store.js ,可以看到 Store 类就出自于这个文件

3. Store类的实现

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

3.1 存放类的状态

首先是定义了一些实例状态,用于存放模块、mutations 、actions 、getters 缓存等东西

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

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

this._makeLocalGettersCache = Object.create(null)   // getters的本地缓存
 

关于各个变量状态的作用都写在这了,其中只有 this._modules = new ModuleCollection(option) 执行了一些操作,其作用就是进行模块递归收集,根据 ModuleCollection 的来源,我们移步到 ./module/module-collection.js 文件

3.1.1 递归收集模块

在 Module-collection.js 文件中定义了 ModuleCollection 类,其作用就是通过递归遍历 options 入参,将每个模块都生成一个独立的 Moudle

这里先来熟悉一下 options 的结构,如下:

import Vuex from 'vuex'

const options = {
  state: {...},
  getters: {...},
  mutations: {...},
  actions: {...},
  modules: {
    ModuleA: {
      state: {...},
      ...
      modules: {
        ModuleA1: {...}
      }
    },
    ModuleB: {
      state: {...},
      ...
      modules: {
        ModuleB1: {...}
      }
    }
  }
}

const store = new Vuex.Store(options)

export default store

可以看到传入的 options 整体可以看成一个根模块 root ,然后 root 的 modules 中嵌套着另外两个子模块:ModuleA 和ModuleB ,而 ModuleA 和ModuleB 内部也分别嵌套着一个子模块,分别为 ModuleA1 、ModuleB1 。这样就组成了一个模块树,因此 ModuleCollection 类的工作就是将保留原来的模块关系,将每个模块封装到一个 Module 类中

export default class ModuleCollection {
  constructor (rawRootModule) {
    // 递归注册模块
    this.register([], rawRootModule, false)
  }
  
  // 根据路径顺序,从根模块开始递归获取到我们准备添加新的模块的父模块
  get (path) {
    return path.reduce((module, key) => {
      return module.getChild(key)
    }, this.root)
  }
  
  // 递归注册模块
  register (path, rawModule, runtime = true) {
    if (__DEV__) {
      assertRawModule(path, rawModule)
    }
    
    const newModule = new Module(rawModule, runtime)  // 初始化一个新的模块
    if (path.length === 0) {    // 当前没有别的模块
      this.root = newModule     // 则此模块为根模块
    } else {    // 有多个模块     
      const parent = this.get(path.slice(0, -1))   // 获取到新模块从属的父模块,所以是path.slice(0, -1),最后一个元素就是我们要添加的子模块的名称
      parent.addChild(path[path.length - 1], newModule)    // 在父模块中添加新的子模块
    }

    if (rawModule.modules) {     // 如果有嵌套模块
      /**
       *  1. 遍历所有的子模块,并进行注册;
       *  2. 在path中存储除了根模块以外所有子模块的名称
       *  */ 
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

函数作用:

  1. register(path, rawModule, runtime):注册新的模块,并根据模块的嵌套关系,将新模块添加作为对应模块的子模块
  • path:表示模块嵌套关系。当前为根模块时,没有任何嵌套关系,此时 path = [] ; 当前不是根模块时,存在嵌套关系,例如上述例子中的 ModuleA1 ,它是 ModuleA 的子模块 ,而 ModuleA 又是根模块的子模块,此时 path = ['ModuleA', 'ModuleA1']
  • rawModule:表示模块对象,此时是一个对象类型
  • runtime:表示程序运行时
  1. get(path):根据传入的 path 路径,获取到我们想要的 Module 类

ModuleCollection 的构造函数中调用了 register 函数,前两个参数分别为:[] 、rawRootModule ,此时肯定是从根模块开始注册的,所以 path 里无内容,并且 rawRootModule 指向的是根模块

然后来看一下 register 函数里的逻辑。

  1. 首先将当前要注册的模块生成一个 Module ,并将 rawModule 作为参数,用于存放 Module 的信息
  2. 然后通过 if(path.length === 0) 判断是否为根模块,是的话就将 this.root 指向 Module ; 否则就跳到第3步
  3. 判断当前模块不是根模块,就通过 get 函数找到当前模块的父模块,然后调用父模块中的 addChild 方法将当前模块添加到子模块中
  4. 最后再判断当前模块是否还有嵌套的模块,有的话就重新回到第1步进行递归操作 ; 否则不做任何处理

按照上面的逻辑,就可以将所有的模块递归收集并注册好了,其中有一个 Module 类还没有具体提到,所以这里移步到 ./module/module.js

import { forEachValue } from '../util'

// 定义了Vuex中的 Module 类,包含了state、mutations、getters、actions、modules
export default class Module {
  constructor (rawModule, runtime) {
    this.runtime = runtime
    
    this._children = Object.create(null)   // 创建一个空对象,用于存放当前模块的子模块
    
    this._rawModule = rawModule         // 当前模块的一些信息,例如:state、mutations、getters、actions、modules
    const rawState = rawModule.state    // 1. 函数类型 => 返回一个obj对象; 2. 直接获取到obj对象

    // 存储当前模块的state状态
    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)
    }
  }
}

来看一下刚才模块收集时,创建的 Module 类内部做了什么事情,同样的从 constructor 中开始看this._children 是一个对象值,用于存放该模块嵌套的其它 Module 类 ;

this._rawModule 就是用于存放该模块内部的一些信息,例如:state 、mutations 、actions 、getters 、moudles ;

this.state 对应的就是 this._rawModule 中的 state ;

这是整个构造函数中执行的操作,我们可以看到,在生成一个 Module 类的时候,其只定义了 state 属性,而 mutations 、getters 、actions 、modules 都是没有被定义的,即例如现在是无法通过 Module.mutations 获取到该模块所有的 mutations 方法,那么这些方法都是在何时被定义的呢?自然是等模块全部都收集完毕以后才进行的操作,因为 vuex 中的嵌套模块可能会存在命名空间 namespaced

3.2 注册模块

到此为止,各个模块的类都创建好了,那么继续回到 ./src/store.jsconstructor 构造函数中

// 将 dispatch 和 commit 方法绑定到 Store 的实例上,避免后续使用dispatch或commit时改变了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)
}

// 判断store是否未严格模式。true: 所有的state都必须经过mutations来改变
this.strict = strict

// 将根模块的state赋值给state变量
const state = this._modules.root.state

这段代码首先对 Store 实例上的 dispatch 和 commit 方法进行了一层包装,即通过 call 将这两个方法的作用对象指向当前的 Store 实例,这样就能防止后续我们操作时,出现 this.$store.dispatch.call(obj, 1) 类似的情况而报错

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

const state = this._modules.root.state 获取的是根模块的 state ,用于后续的一些操作

一切都准备就绪了,下面就开始为每个模块注册信息了

// 从根模块开始,递归完善各个模块的信息
installModule(this, state, [], this._modules.root)

调用了installModule方法,并将store实例对象 、state属性 、路径 、根模块对象依次作为参数进行传递

// 注册完善各个模块内的信息
function installModule (store, rootState, path, module, hot) {
  const isRoot = !path.length  // 是否为根模块
  const namespace = store._modules.getNamespace(path)  // 获取当前模块的命名空间,格式为:second/ 或 second/third/

  // 如果当前模块设置了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
  }

  // 如果不是根模块,将当前模块的state注册到其父模块的state上
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取父模块的state
    const moduleName = path[path.length - 1]   // 当前模块的名称
    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('.')}"`
          )
        }
      }
      // 将当前模块的state注册在父模块的state上,并且是响应式的
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 设置当前模块的上下文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)
  })
}

const namespace = store._modules.getNamespace(path)是将路径path作为参数, 调用ModuleCollection类实例上的getNamespace方法来获取当前注册对象的命名空间的

/**
* 根据模块是否有命名空间来设定一个路径名称
* 例如:A为父模块,B为子模块,C为子孙模块
* 1. 若B模块命名空间为second,C模块未设定命名空间时; C模块继承了B模块的命名空间,为 second/
* 2. 若B模块未设定命名空间, B模块命名空间为third; 则此时B模块继承的是A模块的命名空间,而C模块的命名空间路径为 third/
*/
getNamespace (path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)   // 获取子模块
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}

从这可以看出,未指定命名空间的模块会继承父模块的命名空间

  // 如果当前模块设置了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
}

这段代码是将所有存在命名空间的模块记录在store._modulesNamespaceMap中,便于之后的辅助函数可以调用(这里还未提到辅助函数,可以先不管,到时候回头来看)

3.2.1 注册模块的state

// 如果不是根模块,将当前模块的state注册到其父模块的state上
if (!isRoot && !hot) {
  const parentState = getNestedState(rootState, path.slice(0, -1)) // 获取父模块的state
  const moduleName = path[path.length - 1]   // 当前模块的名称
  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('.')}"`
        )
      }
    }
    // 将当前模块的state注册在父模块的state上,并且是响应式的
    Vue.set(parentState, moduleName, module.state)
  })
}

这段代码主要是将非根模块的 state 挂载到父模块的 state 上

const parentState = getNestedState(rootState, path.slice(0, -1)) 根据当前的模块路径,从根模块的 state 开始找,最终找到当前模块的父模块的 state,可以看一下 getNestedState 方法内部的具体实现

// 获取到嵌套的模块中的state
function getNestedState (state, path) {
  return path.reduce((state, key) => state[key], state)
}

这段代码中最主要的部分就是 Vue.set(parentState, moduleName, module.state) ,作用就是调用了 Vue 的 set 方法将当前模块的 state 响应式地添加到了父模块的 state 上,这是因为在之后我们会看到 state 会被放到一个新的 Vue 实例的 data 中,所以这里不得不使用 Vue 的 set 方法来响应式地添加

同样的,从这段代码中我们也可以知道了为什么平时在获取子模块上 state 的属性时,是通过 this.$store.state.ModuleA.name 这样的形式来获取的了

3.2.2 生成模块调用上下文

// 设置当前模块的上下文context
const local = module.context = makeLocalContext(store, namespace, path)

这行代码也可以说是非常核心的一段代码了,它根据命名空间为每个模块创建了一个属于该模块调用的上下文,并将该上下文赋值了给了该模块的 context 属性

接下来看一下这个上下文是如何创建的吧

// 若设置了命名空间则创建一个本地的commit、dispatch方法,否则将使用全局的store
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) {  // 若传入了第三个参数设置了root:true,则派发的是全局上对应的的actions方法
        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)
    }
  }

  /**
   * 若没有设定命名空间,则直接读取store.getters(store.getters已经挂载到vue实例的computed上了);
   * 若设定了命名空间,则从本地缓存_makeLocalGettersCache中读取getters
   */
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters    
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}

local 这个变量存储的就是一个模块的上下文。

先来看其第一个属性 dispatch ,当该模块没有设置命名空间时,调用该上下文的 dispatch 方法时会直接调用 sotre.dispatch ,即调用了根模块的 dispatch 方法 ; 而存在命名空间时,会先判断相应的命名空间,以此来决定调用哪个 dispatch 方法

if (!options || !options.root) 是判断调用 dispatch 方法时有没有传入第三个参数 {root: true} ,若有则表示调用全局根模块上对应的的 dispatch 方法

那么同样的,local 中的 commit 属性就类似于 dispatch ,这里就不多说了

然后最后通过 Object.defineProperties 方法对 local 的 getters 属性和 state 属性设置了一层获取代理,等后续对其访问时,才会进行处理。例如,访问 getters 属性时,先判断是否存在命名空间,若没有,则直接返回 store.getters ; 否则的话,根据命名空间创建一个本地的 getters 缓存,根据这个缓存来获取对应的 getters ,来看一下代码

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

      // 获取本地getters名称
      const localType = type.slice(splitPos)

      // 对getters添加一层代理
      Object.defineProperty(gettersProxy, localType, {
        get: () => store.getters[type],
        enumerable: true
      })
    })
    // 把代理过的getters缓存到本地
    store._makeLocalGettersCache[namespace] = gettersProxy
  }

  return store._makeLocalGettersCache[namespace]
}

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

上下文已经创建好了,接下来就是注册 mutations 、actions 、getters 了

3.2.3 注册模块的mutations

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

这里遍历了模块的所有 mutations 方法,通过命名空间 + mutations 方法名的形式生成了 namespacedType

然后跳到 registerMutations 方法看看具体是如何注册的

// 注册mutations方法
function registerMutation (store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = [])  // 通过store._mutations 记录所有注册的mutations
  entry.push(function wrappedMutationHandler (payload) {
    handler.call(store, local.state, payload)
  })
}

首先根据我们传入的 type 也就是上面的 namespacedType 去 store._mutations 寻找是否有入口 entry ,若有则直接获取 ; 否则就创建一个空数组用于存储 mutations 方法

在获取到 entry 以后,将当前的 mutations 方法添加到 entry 末尾进行存储。其中 mutations 接收的参数有两个,即 上下文中的 state 和 我们传入的参数 payload

从这段代码我们可以看出,整个 store 实例的所有 mutations 方法都是存储在 store._mutations 中的,并且是以键值对的形式存放的,例如:

store._mutations = {
  'mutations1': [function handler() {...}],
  'ModuleA/mutations2': [function handler() {...}, function handler() {...}],
  'ModuleA/ModuleB/mutations2': [function handler() {...}]
}

其中键是由命名空间和 mutations 方法名组成的,值是一个数组,存放着所有该键对应的 mutations 方法

为什么是用数组存放呢?因为在上面说过,假设父模块ModuleA 里有一个叫 func 的 mutations 方法,那么其在 store._mutations 中就是这个样子的

store._mutations = {
  'ModuleA/func': [function handler() {...}]
}

若子模块没有设置命名空间,那么他是会继承父模块的命名空间的,此时子模块里也有一个叫 func 的 mutations 方法,那么在获取 entry 时,获取到的是 store._mutations['ModuleA/func'] ,但此时这个 entry 中已经有一个 mutations 方法了,那么为了保证之前的方法不被替换,就选择添加到数组的末尾,此时应该就可以猜测到了,后续如果调用该 mutations 方法,会先获取到相应的数组,然后遍历依次执行

得出个结论:mutations 方法是可以重名的

3.2.4 注册模块的actions

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

遍历模块的所有actions方法,其中对于typehandler的处理主要是为了兼容两种写法:

// 第一种写法:
actions: {
  func(context, payload) {
    // 省略业务代码...
  }
}

// 第二种写法:
actions: {
  func: {
    root: true,
    handler(context, payload) {
      // 省略业务代码...
    }
  }
}

当采用第二种写法,并且 root = true 时,就会将该 actions 方法注册到全局上,即前面不加上任何的命名空间前缀

再来看看 registerAction 方法里具体实现了什么

// 注册actions方法,接收两个参数:context(包含了上下文中的dispatch方法、commit方法、getters方法、state)、传入的参数payload
function registerAction (store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = [])   // 通过store._actions 记录所有注册的actions
  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)
    // 若返回值不是一个promise对象,则包装一层promise,并将返回值作为then的参数
    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
    }
  })
}

与 mutations 类似,先从 store._actions 获取入口 entry ,然后将当前的 actions 进行包装处理后添加到 entry 的末尾。 actions 方法接收两个参数,即 context 和我们传入的参数 payload ,其中 context 是一个对象,里面包含了 dispatch 、commit 、getters 、state 、rootGetters 、rootState ,前4个都是在当前模块的上下文中调用的,后2个是在全局上调用的

最后对于 actions 的返回值还做了一层处理,因为 actions 规定是处理异步任务的,所以我们肯定希望其值是一个 promise 对象,这样方便后续的操作。所以这里对 actions 方法的返回值做了一个判断,如果本身就是 promise 对象,那么就直接返回 ;若不是,则包装一层 promise 对象,并将返回值 res 作为参数返回给 .then

同样的,actions 方法也是可以重名的

3.2.5 注册模块的getters

// 注册模块的所有getters
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})

与上面的类似,这里就不多说了,直接跳到registerGetters方法

// 注册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
  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 并不像 mutations 和 actions 一样去获取一个 entry ,而是直接查看 store._wrappedGetters[type] 是否有对应的 getters ,若有,则不再重复记录 ; 否则将 getters 包装一下存在 sotre._wrappedGetters 中,其中经过包装后的 getters 接收4个参数,即 state 、getters 、rootState 、rootGetters ,前2个分别表示当前上下文中的 state 和 getters ,后2个分别表示根模块的 state 和 getters

所以我们在使用 Vuex 时,调用子模块的 getters 时是这样的:

const store = Vuex.Store({
  state: {
    a: 1,
    b: 2
  },
  getters: {
    addA(state) {
      return state.a + 1
    }
  },
  modules: {
    // 子模块A
    ModuleA: {
      state: {
        c: 3
      },
      getters: {
      	sum(state, getters, rootState, rootGetters) {
          console.log(state.c)   // 3
          console.log(getters.addC)  // 4
          console.log(rootState.b)  // 2
          console.log(rootGetters.addA)  // 2
        },
        addC(state) {
          return state.c + 1
        }
      }
    }
  }
})

最后我们再次得出一个结论,getters 是不能重名的,并且前一个命名的不会被后一个命名的所覆盖

3.2.6 递归注册子模块

// 递归注册子模块
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})

然后就是判断当前的模块里有没有嵌套的子模块了,有的话就将子模块的名称添加到 path 末尾,然后把相应的参数传入 installModule 方法,重新走一遍本文中 3.2 里所有的流程

3.3 注册vm

上面已经将模块的注册完毕了,看一下 constructor 中下一行代码是什么

resetStoreVM(this, state)

跳到相应的方法中去看一下:

// 初始化vm
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,将每一个getter注册到store.getters,访问对应getter时会去vm上访问对应的computed
  forEachValue(wrappedGetters, (fn, key) => {
    computed[key] = partial(fn, store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  const silent = Vue.config.silent
  Vue.config.silent = true
  // 使用Vue实例来存储Vuex的state状态树,并利用computed去缓存getters返回的值
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // 启用严格模式的监听警告
  if (store.strict) {
    enableStrictMode(store)
  }

  // 若存在旧的vm, 销毁旧的vm
  if (oldVm) {
    if (hot) {
      // 解除对旧的vm对state的引用
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}

这个方法里主要做的就是生成一个 Vue 的实例 _vm ,然后将 store._makeLocalGettersCache 里的 getters 以及 store.state 交给一个 _vm 托管,即将 store.state 赋值给 _vm.data.$$state ,将 store._makeLocalGettersCache 通过转化后赋值给 _vm.computed ,这样一来,state 就实现了响应式,getters 实现了类似 computed 的功能

因为生成了新的 _vm ,所以最后通过 oldVm.$destory() 将旧的 _vm 给销毁掉了

值得注意的是,其将 sotre.getters 的操作放在了这个方法里,是因为我们后续访问某个 getters 时,访问的其实是 _vm.computed 中的内容。因此,通过 Object.defineProperty 对 store.getters 进行了处理

3.4 访问 state 、mutations 、actions

到此为止,已经实现了可以通过 store.getter.某个getters 来使用 getters ,那么如何访问 state 、mutations 、actions 呢?

3.4.1 访问 state

通过搜索,在 Store 类中定义了一个 get 函数,用于处理 store.state 的操作:

get state () {
  return this._vm._data.$$state
}

可以很清楚地看到,当我们访问 store.state 时,就是去访问 store._vm.data.$$state ,与刚才介绍 _vm 时说的一样

3.4.2 访问 mutations

其实 mutations 的访问在一开始就触及到了,只不过当时只是提了一嘴,因为当时直接来看可能不会太明白

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

Store中,对store.commitstore.dispatch方法做了一层处理,将该方法的调用指向了store,先来看看commit方法的具体实现

commit (_type, _payload, _options) {
  // check object-style commit
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  const entry = this._mutations[type]    // 查找_mutations上是否有对应的方法
  // 查找不到则不执行任何操作
  if (!entry) {
    if (__DEV__) {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }

  // 若有相应的方法,则执行
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })

  this._subscribers
    .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'
    )
  }
}

首先通过unifyObjectStyle方法对传入的参数进行了处理,来看一下这个方法是干什么的

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

使用过Vuex的应该都知道,commit有两种提交方式:

// 第一种提交方式
this.$store.commit('func', 1)

// 第二种提交方式
this.$store.commit({
  type: 'func',
  num: 1
})

其先对第一个参数进行判断是否为对象,是的话就当作对象提交风格处理,否则的话就直接返回

在处理完参数以后,根据 type 从 store._mutations 上获取到 entry ,前面分析过了,mutations 方法是以数组形式存储的,所以可能有多个方法。然后在 _withCommit 方法中遍历 entry 依次执行 mutations 方法,这是因为 Vuex 规定 state 的改变都要通过 mutations 方法,store._committing 这个属性就是用来判断当前是否处于调用 mutations 方法的,当 state 值改变时,会先去判断 store._committing 是否为 true ,若不为 true ,则表示 state 的值改变没有经过 mutations 方法,于是会打印警告⚠️ 信息

而 this._subscribers 这段代码我暂时还不清楚是干什么的,通过词义,目测应该是一个存放订阅的东西吧,就先放着不管了,等后续回来再看

3.4.3 访问 actions

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

  const result = entry.length > 1
  ? Promise.all(entry.map(handler => handler(payload)))
  : entry[0](payload)

  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 方法类似,就不多说了

代码中又出现了 this._actionSubscribers ,与 commit 中的也类似,可能这里是存放 actions 的订阅者的东西,所以这些都先不看了

其中变量 result ,先判断 entry 的长度,若大于1,则表示有多个异步方法,所以用 Promise.all 进行包裹 ; 否则直接执行 entry[0]

最后创建并返回了一个新的 promise ,内部判断了 result 的状态,成功则执行 resolve ,失败则执行 reject

到此为止,我们已经实现了 store.state 、store.getters 、store.commit 、store.dispatch 的调用了

3.5 插件的调用

继续看 constructor 中的代码(这段代码也是整个 Store 类的构造函数中最后的一小段代码了)

// 依次调用传入的插件
plugins.forEach(plugin => plugin(this))

const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
// 使用vue的开发插件
if (useDevtools) {
  devtoolPlugin(this)
}

首先就是遍历创建 Store 类时传入的参数 Plugins ,依次调用传入的插件函数(当然一般我们都没有传入,所以 Plugins 默认是空数组)

然后就是调用 devtoolPlugin 方法啦,根据导入的路径我们去到相应的文件

// 文件路径:./plugins/devtool.js
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

  devtoolHook.emit('vuex:init', store)

  devtoolHook.on('vuex:travel-to-state', targetState => {
    store.replaceState(targetState)
  })

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

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

看了半天,搜索了半天,都没有找到哪个文件里有 __VUE_DEVTOOLS_GLOBAL_HOOK__ ,应该是 dev-tools 插件里定义的,为了保证 Vuex 的源码阅读进度,就先舍弃阅读 dev-tools 插件的内容了

3.6 其它方法

整个 Store 实例生成的全过程差不多就是这样了,另外还会发现,其实有很多方法都没有被用到,但是却被定义出来了,这里可以稍微列举几个简单地看一下

3.6.1 更新 state

// 在store._committing = true 的状态下更新一下state
replaceState (state) {
  this._withCommit(() => {
    this._vm._data.$$state = state
  })
}

一目了然,这是提供了一种直接修改state的方法,并且不会打印警告信息

3.6.2 注册、卸载模块

// 注册模块
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)
}

3.6.3 重置 store 实例

// 重置store,即注册模块、生成vm等操作
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
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}

将所有的状态都清空,然后重新执行一边 installModule 和 resetStoreVM ,这一般在模块结构变化以后调用,例如某个模块被卸载

4. install 注册

Store 类的所有实现都了解完了,再来看一下入口文件里还有什么,突然发现忘记看一下非常重要的 install 方法了,根据 install 方法的导入路径找到相应的函数:

// 提供install方法
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    if (__DEV__) {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

当我们调用Vue.use(vuex)时,调用这个方法,先判断vuex是否已被注册,若已被注册,则不执行任何操作 ; 若没有被注册,则调用applyMixin方法,现在移步到./mixin.js文件:

export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  // 2.x版本直接通过全局混入Vue.mixin的方式挂载store
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // 兼容1.x版本
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  // 将vuex混入到$options中
  function vuexInit () {
    // 获取当前组件的 $options
    const options = this.$options
    // 若当前组件的$options上已存在store,则将$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
    }
  }
}

applyMixin 方法先判断了 Vue 的版本号,主要做的是一个向下兼容 Vue 1.x 的版本,这里我对 Vue 1.x 的版本不太熟悉,所以就直接看 Vue 2.x 版本的处理方式吧

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

这里不得不感叹,这个处理方式太棒了。

5. 辅助函数

store实例生成并且也 install 到 Vue 上了,看一下入口文件中只剩下辅助函数了,它们有 mapState 、mapGetters 、mapMutations 、mapActions 、createNamespacedHelpers ,进到相应的文件 ./helpers.js 中看一下

import { isObject } from './util.js'

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  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]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})


export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  if (__DEV__ && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  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
})


export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  if (__DEV__ && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (__DEV__ && !(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
})


export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  if (__DEV__ && !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  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
})

/**
 * Rebinding namespace param for mapXXX function in special scoped, and return them by simple object
 * @param {String} namespace
 * @return {Object}
 */
export const createNamespacedHelpers = (namespace) => ({
  mapState: mapState.bind(null, namespace),
  mapGetters: mapGetters.bind(null, namespace),
  mapMutations: mapMutations.bind(null, namespace),
  mapActions: mapActions.bind(null, namespace)
})


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

function isValidMap (map) {
  return Array.isArray(map) || isObject(map)
}

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

function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}

整个文件里东西非常多,但我们很明确地知道,我们主要看的就是那几个辅助函数,观察发现,每个辅助函数都会先调用normalizeNamespace函数进行处理,那么我们就先看看这个函数做了什么:

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

根据函数名的字面意思知道这应该是根据不同的调用方法,标准化命名空间的。

首先返回一个函数,接收两个参数,即 namespace 和 map ,这也是我们调用辅助函数时可以传入的两个参数 ;

然后判断 namespace 是否为字符串形式,若不是字符串,则表示是普通的调用方式,例如:

mapMutations(['first/second/foo', 'first/second/bar'])

mapMutations({
   foo: 'first/second/foo',
   bar: 'first/second/bar',
})

这种情况,就直接将第一个参数 namespace 赋值给映射变量 map ,而 namespace 设为空

若是字符串的话,则表示调用的是带命名空间的绑定函数的,例如:

mapState('first/second', ['foo', 'bar'])

mapState('first/second', {
  foo: 'foo',
  bar: 'bar',
})

处理好这两种不同的调用方式以后,调用一下 fn ,并将 namespacemap 作为参数

那么就先从 mapState 开始看吧

5.1 mapState

export const mapState = normalizeNamespace((namespace, states) => {
  const res = {}
  if (__DEV__ && !isValidMap(states)) {
    console.error('[vuex] mapState: mapper parameter must be either an Array or an Object')
  }
  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]
    }
    // mark vuex getter for devtools
    res[key].vuex = true
  })
  return res
})

这里的 namespace 是一个字符串,states 是我们刚才处理好的映射变量 map

首先创建一个空对象 res ,这是我们最后处理好要返回的变量 ;

然后通过 isValidMap 方法判断 map 是否符合要求,即是否是数组或对象 ;

再然后调用了 normalizeMap 方法处理了变量 states ,从字面意义上来看,这是用来标准化该变量的,因为毕竟有可能是数组又有可能是对象嘛,所以要统一一下。来看一下 normalizeMap 方法的实现:

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

首先仍然要先判断 map 是否合法,若不合法,则返回空数组,避免后续的代码报错 ;

然后判断 map 是否为数组,若是数组,则遍历 map 进行处理:

将 [1, 2, 3] 变成 [{key: 1, val: 1}, {key: 2, val: 2}, {key: 3, val: 3}]

处理好了以后就直接返回,在得到标准化以后的 map 后要对其进行 forEach 遍历,将遍历到的每一个对象经过处理后存放在 res 中,即 res[key] = function mappedState() {...} ,来看一下这个 mappedState 里做了什么处理

首先获取一下根模块上的 state 和 getters

// 获取根模块的 state 、getters
let state = this.$store.state
let getters = this.$store.getters

然后判断是否存在命名空间,即namespace是否为空,若为空,则不做任何处理 ; 否则调用getModuleByNamespace方法获取到namespace对应的模块module

function getModuleByNamespace (store, helper, namespace) {
  const module = store._modulesNamespaceMap[namespace]
  if (__DEV__ && !module) {
    console.error(`[vuex] module namespace not found in ${helper}(): ${namespace}`)
  }
  return module
}

可以看到 store._modulesNamespaceMap 终于派上了用场,在生成 Store 实例注册所有模块的时候,将带有命名空间的模块都存储在了该变量上,原来是在这里用上了

然后将刚才声明的变量 state 和 getters 替换成 module 对应上下文中的 state 和 getters

if (namespace) {
  // 获取命名空间namespace对应的模块
  const module = getModuleByNamespace(this.$store, 'mapState', namespace)
  if (!module) {
    return
  }
  // 将 state 、getters 变成该模块上下文中的 state 、getters
  state = module.context.state
  getters = module.context.getters
}

这个context也是非常的巧妙,在注册模块的时候,获取到该模块的上下文的同时,还将其存储了一下,即:

const local = module.context = makeLocalContext(store, namespace, path)

之前看到的时候不知道有啥用,但在这里看到后,觉得真的非常得赞 ?

确定好了 stategetters 的值,最后就可以返回值了

return typeof val === 'function'
  ? val.call(this, state, getters)
	: state[val]

这里还做了一层处理是因为要处理两种不同的方式,例如:

mapState({
  foo: state => state.foo,
  bar: 'bar'
})

在这里我又发现了一个官方文档里没有提及的,就是以函数形式返回的时候,还能接收第二个参数 getters ,即:foo: (state, getters) => state.foo + getters.bar

5.2 mapMutations

export const mapMutations = normalizeNamespace((namespace, mutations) => {
  const res = {}
  if (__DEV__ && !isValidMap(mutations)) {
    console.error('[vuex] mapMutations: mapper parameter must be either an Array or an Object')
  }
  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
})

mapMutationsmapState的实现大体相似,主要的不同就在下面这段代码:

return typeof val === 'function'
  ? val.apply(this, [commit].concat(args))
	: commit.apply(this.$store, [val].concat(args))

这里也是像mapState一样处理了函数的调用类型和普通的调用类型,例如:

mapMutations({
  foo: (commit, num) => {
    commit('foo', num)
  },
  bar: 'bar'
})

当是函数的调用类型时,则将 commit 作为第一个参数,并把额外的参数一并传入,所以才有的 val.apply(this, [commit].concat(args)) 这段代码 ;

当是普通的调用类型时,则直接执行 commit ,其中 val 对应的就是该命名空间下需要调用的 mutations 方法名,然后再接收额外的参数,即 commit.apply(this.$store, [val].concat(args))

5.3 mapGetters

export const mapGetters = normalizeNamespace((namespace, getters) => {
  const res = {}
  if (__DEV__ && !isValidMap(getters)) {
    console.error('[vuex] mapGetters: mapper parameter must be either an Array or an Object')
  }
  normalizeMap(getters).forEach(({ key, val }) => {
    // The namespace has been mutated by normalizeNamespace
    val = namespace + val
    res[key] = function mappedGetter () {
      if (namespace && !getModuleByNamespace(this.$store, 'mapGetters', namespace)) {
        return
      }
      if (__DEV__ && !(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
})

这个也没什么好说的了,拿到命名空间 namespace ,直接拼接上 val 通过 this.$store.getters[val] 进行访问。简单举个例子:

第一种情况

// 第一种
mapGetters(['first/foo'])

这种情况下 namespace 被处理成了空字符串,map 被处理成了 ['first/foo'] ,遍历 map ,此时 val = 'first/foo' ,那么 val = namespace + val 处理后 val 仍然等于 first/foo ,所以最后就相当于调用 this.$store.getters['first/foo']

再来看第二种情况

// 第二种
mapGetters('first', ['foo'])

这种情况下 namespace 被处理成了 first/,map 被处理成了 ['foo'] ,遍历 map ,此时 val = 'foo' ,那么 val = namespace + val 处理后 val 就等于 first/foo ,所以最后仍然是相当于调用 this.$store.getters['first/foo']

5.4 mapActions

export const mapActions = normalizeNamespace((namespace, actions) => {
  const res = {}
  if (__DEV__ && !isValidMap(actions)) {
    console.error('[vuex] mapActions: mapper parameter must be either an Array or an Object')
  }
  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 几乎一模一样,就不多说了

5.5 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 源码:

  1. Vuex 的核心源码比较少,对于像我一样第一次阅读源码的人比较友好
  2. 深入学习了常用的库以后,在使用的时候遇到问题,可以快速地找到问题根源
  3. 不能只停留在成熟的库的表面,要学习它们的思想、技术,这样有助于自己的成长

刚开始不知道自己能花多久时间看完 Vuex 的核心源码,我初步给自己定了 15 天的期限,预计每天至少看 2 小时。于是我把 Vuex 的源码 fork 并 clone 了下来,第一天简单地找了一下核心代码的位置,然后非常粗略地看了一下源码里的大致流程。同时,我去 Vuex 官方文档里重新仔仔细细地回顾了一下所有的核心使用方法 接下来的时间我就按照我本文的阅读顺序进行源码的阅读

这里总结几点阅读源码的心得体会吧:

  1. 对于这个库的使用一定要十分熟练,即明白各种方法的使用,强烈建议把官方文档吃透(重点)
  2. 找到核心代码的位置,从入口文件开始,一步步看多看
  3. 源码中的英文注释,看不懂的可以用翻译,这些注释基本上能帮你理解这段代码的作用
  4. 到看不懂的地方可以先打个备注,因为它可能与后面的某些代码有所联系,等之后回头来看之前看不懂的代码时,就会明白了
  5. 阅读源码的过程中,看到某些变量或函数时,先看命名,因为这些命名的字面意思基本上就代表了它的作用,然后要学会联想到这个正常的调用是什么样的,这样更便于理解
  6. 多多利用编译器的搜索功能。因为有时你看到的函数或变量可能在别的地方也有用到,为了方便寻找,可以利用好编译器的搜索功能(包括当前本地搜索和全局搜索)

问答环节

这里放上几个群友对于这次阅读源码问我的问题: Q1: 你是怎么看源码的?有看别人的视频或者别人的文章吗? A1: 没有看别人的视频或者文章,就当时自己思考了一下该如何看源码,列了一个步骤,就这样摸索着看完了,觉得还挺有意思的 Q2: 光自己看能看懂吗? A2: 说实话确实有些地方挺难看懂的,但结合着源码自带的英文注释,基本上能把大致的思路理清,然后看不懂的地方就先做上记号并暂时略过,等到看了更多的代码了以后,回过头来就发现似乎看懂了些。最后要说的就是,源码真不是一遍就能看懂的,真的是要反反复复多看几遍,才能理解其中的原理 Q3: 看完源码后,你能自己手写出来吗? A3: emmmm…这可能有点难度,但是我觉得手写一些核心代码,实现一个简陋的 Vuex 还是可以做到的吧,而且我觉得很有必要自己再去手写一下核心代码,因为这又是一次对源码的巩固,并且我也已经开始在写一个简陋版的 Vuex 了,放在仓库的 myVuex 文件夹下

最后

若本文对于 Vuex 源码阅读有任何错误的地方,欢迎大家给我提意见,一定虚心听取你们的指正,觉得本仓库 ➡️ Vuex源码阅读仓库 不错的,也可以点个star 支持一下我。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 从未看过源码,到底该如何入手?分享一次完整的源码阅读过程

    我觉得每个人可能都有过看源码的想法吧,也包括我。因为看源码不光能使自己对这个库更加熟悉,还能学习到作者强大的思想,久而久之,自己的水平和思想也会有明显的提升的。

    @零一
  • 读懂Redis源码,我总结了这7点心得

    用了这么久的 Redis,也翻了很多次源码,经常有人问我到底怎么读 Redis 源码。

    _Kaito
  • 源码阅读的方法、误区以及三种境界

    在 JAVA 领域中笔者认为通用型基础技术包含 JAVA 集合、Java并发(JUC)。这类技术是项目中使用的高频技术,在合适的场景中选用合适的数据结构、选用合...

    张乘辉
  • 优秀工程师必备技能之如何高效阅读源码

    “平时不用看源码, 看源码太费时间,还容易忘记,工作中出现问题再针对性地阅读,效率更高。”

    码哥字节
  • Android开发要达到阿里P7水平,很难吗?凭什么人家能进大厂?

    Android开发前几年火爆一时,市场饱和后Android程序员每一名程序员都想进阶,甚至成为架构师,但这期间,需要付出的辛苦和努力远超过我们的想象。

    Android技术干货分享
  • 『沉浸式Github 使用指南 』

    谢伟
  • 我是如何在短期内快速掌握Dubbo的原理和源码的(纯干货)?

    作者个人研发的在高并发场景下,提供的简单、稳定、可扩展的延迟消息队列框架,具有精准的定时任务和延迟队列处理功能。自开源半年多以来,已成功为十几家中小型企业提供了...

    冰河
  • 一篇文章把本该属于你的源码天赋还给你

    "有人阅读过 mybatis 的源码吗 ?就看一个初始化过程就看的已经头晕眼花了,小伙伴们支支招吧!"

    技术zhai
  • 一篇文章把本该属于你的源码天赋还给你

    "有人阅读过 mybatis 的源码吗 ?就看一个初始化过程就看的已经头晕眼花了,小伙伴们支支招吧!"

    技术zhai
  • spark源码阅读基本思路

    a.解决企业中bug。比如flink早期bug,就很多,如json序列化工具,在开启flink仅一次处理,json格式不符合要求,就会抛异常而挂掉,然后重试,挂...

    Spark学习技巧
  • 编程中,有哪些好的习惯一开始就值得坚持?

    嗨,小伙伴们大家好,我是沉默王二,就是那个吹自己既有颜值又有才华的家伙。今天这个话题我觉得还是值得和大家探讨的,因为好的编程习惯对程序员的职业生涯真的非常重要。

    沉默王二
  • 分析开源项目源码,我们该如何入手分析?(授人以渔)

    本文接上篇文章跟大家聊聊我们为什么要学习源码?学习源码对我们有用吗?,那么本篇文章再继续跟小伙伴们聊聊源码这个话题。

    源码笔记
  • 「方法论」我们如何看源码

    跟小伙伴们聊聊“分析开源项目源码,我们该如何入手分析?”这个话题,我们就随便扯皮,反正是跟小伙伴们一起学习交流,没必要太正式。

    Java3y
  • 怎么利用好公众号后台做好分析报告

    报告显示,到目前为止,微信及WeChat的合并月活跃账户高达11.12亿,相当于每2个中国人就有一人使用微信。而根据腾讯微信官方消息,截止2019年8月,微信公...

    用户6102055
  • Linux内核源码分析方法

    Linux内核源码分析方法 一、内核源码之我见 Linux内核代码的庞大令不少人“望而生畏”,也正因为如此,使得人们对Linux的了解仅处于泛泛的层次。如果想透...

    Florian
  • 如何在移动开发者的寒冬中破冰而出?

    在这个行业,每隔一段时间都会出现新的技术,好焦虑呀,到底该学什么?14年移动开发出现了大量的泡沫,随便一个培训机构培训几个月出来的在北上广都是8k起,现在感觉移...

    陈宇明
  • 深度解密Go语言之channel

    大家好啊!“深度解密 Go 语言”系列好久未见,我们今天讲 channel,预祝阅读愉快!在开始正文之前,我们先说些题外话。

    梦醒人间
  • [非广告]打工人如何持续提升自己的技术能力?

    作为打工人,只有不断的为企业解决各种问题,才能体现你的价值,然而,只有当你能够为企业解决越来越大的问题的时候,才能体现你的核心竞争力。

    彤哥
  • 从外包到大厂的逆袭!安卓开发5年,一年蛰伏后面试成offer收割机,入职腾讯定薪20*15

    14年刚毕业的时候,Android开发市场发展火热,无数人员涌入Anroid开发行业,人员增长率快,自然市场竞争力就大。当因为学历不高、职业技能不熟悉再加上没经...

    Android技术干货分享

扫码关注云+社区

领取腾讯云代金券