深海鱼的博客 深海鱼的博客
首页
  • 《ES6 教程》
  • 《TypeScript》
  • 《Vue》
  • 《React》
  • 《Git》
  • Javascript
  • CSS
手写系列
  • React项目实战
  • Vue3项目实战
  • 服务端项目实战
  • 鸿蒙项目实战
  • 小小实践
  • Vue全家桶

    • Vue2.0
    • Vue3.0
    • VueRouter
    • Vuex
  • React

    • React
  • Axios
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

深海鱼

尽人事,知天命,知行合一。
首页
  • 《ES6 教程》
  • 《TypeScript》
  • 《Vue》
  • 《React》
  • 《Git》
  • Javascript
  • CSS
手写系列
  • React项目实战
  • Vue3项目实战
  • 服务端项目实战
  • 鸿蒙项目实战
  • 小小实践
  • Vue全家桶

    • Vue2.0
    • Vue3.0
    • VueRouter
    • Vuex
  • React

    • React
  • Axios
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 导读
  • vue2

  • vuex

    • 前言
    • 调式环境准备
    • 源码基本结构与install函数
    • ModuleCollection与Module类
      • 一、简述Store类的构造函数
      • 二、Module类
        • 1. 构造函数
        • 2. 访问属性namespaced
        • 3. 模块的增删查改
        • 4. 各种遍历方法
        • 5. Module类总结
      • 三、ModuleCollection类
        • 1. 构造函数
        • 2. 模块注册register
        • 3. 模块卸载unregister
        • 4. 更新模块
        • 5. 其它实例方法
        • 6. 参数校验
        • 7. ModuleCollection总结
    • Store实例化
    • Store实例方法
    • 辅助函数
    • 内置插件
    • 总结与常见问题
  • vue-router

  • vue3

  • react

  • Axios

  • 源码解读
  • vuex
深海鱼
2024-06-26
目录

ModuleCollection与Module类

上一篇文章《源码基本结构与install函数》中我们已经详细讲解了Vuex的整个源码的基本结构以及install函数的实现。按照读源码的节奏我们接下来应该分析Store类的实现。但是,我在编写关于《Store实例化》这篇文章时,发现由于Store类的初始化依赖ModuleCollection和Module类,为了让文章的表达更加清晰,我需要假设由这两个类创建的实例的存在,并用大量的注释来解释ModuleCollection和Module实例的结构,进而使源码的分析得以进行。正因如此,会使《Store实例化》这篇文章显得十分冗杂。因此,在编写文章时,我先把ModuleCollection和Module类的实现提到前面来讲解,目的是为了后面讲解Store类的实现时能够更加清晰顺利。

当然,你在实际中阅读源码时应该先阅读Store实例化的实现,再阅读ModuleCollection和Module类的实现。阅读时,你可以通过断点调试的方式来查看ModuleCollection和Module类的实例结构即可。

# 一、简述Store类的构造函数

在Store类构造函数中有如下代码:

constructor(options = {}) {
  // ...
  // 初始化各种实例属性
  this._modules = new ModuleCollection(options) // 创建根模块树
  // ...
  installModule(this, state, [], this._modules.root) // 安装模块

  resetStoreVM(this, state) // 响应式处理

  // 安装插件
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12

Store实例化时,会经过以下几个步骤:

  • 初始化各种实例属性
  • 创建根模块树并赋值给实例属性_modules
  • 根据根模块树,安装模块。安装时会递归安装子模块
  • 响应式处理
  • 安装插件

在实例化的构成中,安装模块和响应式处理是Store类的核心,这两个操作都依赖ModuleCollection类创建的_modules实例属性。而Module类本质上是用来创建一个封装了增删改查方法的实例对象,它被ModuleCollection类用来创建模块树。我们后文先从Module类开始讲解,再讲解ModuleCollection类。

# 二、Module类

Module类是Vuex中用来创建模块实例的类,其定义在文件module/module.js。Module类代码:

export default class Module {
  constructor(rawModule, runtime) {

  } 
  get namespaced() {

  }
  // ...
}

1
2
3
4
5
6
7
8
9
10

# 1. 构造函数

constructor(rawModule, runtime) {
  this.runtime = runtime // 是否运行时
  this._children = Object.create(null) // 存放子模块
  this._rawModule = rawModule // 记录模块原始的配置对象
  const rawState = rawModule.state // 模块原始的state
  // 初始化state, state是函数时则执行函数并返回结果,否则直接返回state,默认值是{}
  this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
1
2
3
4
5
6
7
8

Module类的构造函数是比较简单的,它接收两个参数:

  • rawModule:原始模块对象,在实例化时传入的是模块的配置对象,其中包含了state、mutations、actions、getters、moudles等属性。
  • runtime:是否运行时,用来标记模块是静态注册的还是动态注册的,后文详解

构造主要完成了以下几件事情:

  • 增加runtime,用来标记模块是静态注册的还是动态注册的
  • 创建_children属性,用来存放子模块
  • 创建_rawModule属性,该属性记录了传入的原始的配置对象
  • 增加state属性,该属性从rawModule.state获取,如果state属性是函数,则执行函数并返回结果,否则直接返回state属性,默认值是{}。

# 2. 访问属性namespaced

get namespaced() {
  return !!this._rawModule.namespaced
}
1
2
3

当访问namespaced属性时,会返回_rawModule.namespaced转为布尔值后的结果。

# 3. 模块的增删查改

// 添加子模块
addChild(key, module) {
  this._children[key] = module
}
// 删除指定子模块
removeChild(key) {
  delete this._children[key]
}

// 获取指定子模块
getChild(key) {
  return this._children[key]
}

// 更新模块,只会更新模块的actions、mutations、getters、namespaced
update(rawModule) {
  this._rawModule.namespaced = rawModule.namespaced // 更新namespaced标记
  if (rawModule.actions) { // 更新actions
    this._rawModule.actions = rawModule.actions
  }
  if (rawModule.mutations) { // 更新mutations
    this._rawModule.mutations = rawModule.mutations
  }
  if (rawModule.getters) { // 更新getters
    this._rawModule.getters = rawModule.getters
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

其中增删查都是根据给定的key值,对实例的_children属性进行操作。而更新操作是更新实例的_rawModule属性。在更新操作中,我们发现,更新操作只会更新actions、mutations、getters、namespaced四个属性,而state并没有包含在内,为什么呢?

# 4. 各种遍历方法

在Module类中,还有如下几个遍历相关属性后执行回调的方法:

// 遍历子模块并执行指定的操作, fn会接收子模块及其子模块的key作为参数
forEachChild(fn) {
  forEachValue(this._children, fn)
}

// 遍历getters并执行指定的操作, fn会接收getters及其key作为参数
forEachGetter(fn) {
  if (this._rawModule.getters) {
    forEachValue(this._rawModule.getters, fn)
  }
}

// 遍历actions并执行指定的操作, fn会接收actions及其key作为参数
forEachAction(fn) {
  if (this._rawModule.actions) {
    forEachValue(this._rawModule.actions, fn)
  }
}

// 遍历mutations并执行指定的操作, fn会接收mutations及其key作为参数
forEachMutation(fn) {
  if (this._rawModule.mutations) {
    forEachValue(this._rawModule.mutations, fn)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

forEachValue函数是一个遍历对象并执行指定回调的函数,并将对象的key和value作为参数传递给回调函数,定义如下:

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

可以看到,这些方法分别都接收一个回调函数,分别遍历_children、_rawModule.getters、_rawModule.actions、_rawModule.mutations这四个属性,并执行指定的回调。

# 5. Module类总结

以上就是Module类完整的代码,可以看到是比较简单的。其主要功能就是根据传入的配置对象,创建一个封装了增删查改方法的实例对象。一个Module实例最终结构如下:

{
  runtime: false, // 用于模块是标记静态注册还是动态注册的
  state: { // 原始的options.state的获取结果
    count: 0
  },
  _children: {}, // 子模块
  _rawModule: {}, // 传入的原始options配置对象
  get namespaced: () => {}, // 是否开启了命名空间
  addChild: (key, module) => {}, // 添加子模块
  removeChild: (key) => {}, // 删除子模块
  getChild: (key) => {}, // 获取子模块
  update: (rawModule) => {}, // 更新模块
  forEachChild: (fn) => {}, // 遍历子模块_children并执行指定的操作
  forEachGetter: (fn) => {}, // 遍历_rawModule.getters并执行指定的操作
  forEachAction: (fn) => {}, // 遍历_rawModule.actions并执行指定的操作
  forEachMutation: (fn) => {} // 遍历_rawModule.mutations并执行指定的操作
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Module类的基本结构如下图: vuex-module-class

# 三、ModuleCollection类

ModuleCollection类是Vuex中用来根模块树的类,其定义在文件module/module-collection.js:

export default class ModuleCollection {
  constructor(options) {
    // ...
  }
  get(path) {
    // ...
  }
  // ...
}
1
2
3
4
5
6
7
8
9

# 1. 构造函数

constructor(rawRootModule) {
  // register root module (Vuex.Store options)
  // 注册根模块,并标记为静态注册
  this.register([], rawRootModule, false)
}
1
2
3
4
5

构造函数接收一个参数,该参数是根模块的配置对象,在调用new Store(options)时会执行以下代码:

constructor(options = {}) {
  // ...
  this._modules = new ModuleCollection(options) // 创建根模块树
  // ...
}
1
2
3
4
5

即rawRootModule为Vuex.Store传入的options对象。接收参数后,ModuleCollection的构造函数只调用了实例方法register,下面来看看register方法的定义。

# 2. 模块注册register

register(path, rawModule, runtime = true) {
  // 断言传入的vuex配置对象是否合法,对参数类型做校验
  if (process.env.NODE_ENV !== 'production') {
    assertRawModule(path, rawModule)
  }

  // 创建模块实例,Module实例是一个封装了对rawModule操作的实例,
  // 包括子模块的增删查改,以及模块actions,getter,mutations等操作
  const newModule = new Module(rawModule, runtime)
  if (path.length === 0) { // 路径为空则注册根模块
    this.root = newModule
  } else { // 否则注册子模块
    // 获取需要注册的子模块的父模块
    const parent = this.get(path.slice(0, -1))
    // 添加子模块
    parent.addChild(path[path.length - 1], newModule)
  }

  // register nested modules
  // 遍历子模块,递归注册子模块
  if (rawModule.modules) { // 模块中有modules属性则遍历
    forEachValue(rawModule.modules, (rawChildModule, key) => {
      this.register(path.concat(key), rawChildModule, runtime)
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
  • register方法接收三个参数:
    • path:当前模块的路径,类型为数组,当注册根模块时,传入的是空数组
    • rawModule:当前要注册的模块配置对象,当注册根模块时,传入的即为new Vuex.Store(options)中的options对象
    • runtime:用于标记是否是运行时注册(即动态注册),当注册根模块时,其传入的值是false,代表静态注册,区分的原因见下文
  • register方法主要做了以下几件事情:
    • 断言传入的vuex配置对象是否合法,即对参数类型做校验(见下文)
    • 创建一个Module实例,该实例封装了对rawModule的操作
    • 如果当前注册的是根模块,则将newModule赋值给this.root,否则根据指定的路径,逐级从this.root找到相应父模块,然后调用addChild方法为其添加子模块
    • 如果rawModule中有modules属性,说明其具有子模块,则遍历modules,递归调用register方法注册子模块

以下代码为例:

const store = new Vuex.Store({
  state: {
    a: 1,
    b: 2
  },
  getters: {
    total: state => state.a + state.b
  },
  modules: {
    user: {
      namespaced: true,
      state: {
        lastName: 'Foo',
        firstName: 'Bar'
      },
      getters: {
        fullName: state => state.lastName + ' ' + state.firstName
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

经过以上处理后,在Store实例中,其this._modules属性会是类似下面的结构:

this._modules = {
  root: {
    runtime: false,
    state: {}, // 原始的options.state
    _rawModule: {} // 原始的options配置对象
    get namespaced: false,
    _children: {
      user: {
        runtime: false,
        state: {}, // 原始的options.modules.user.state
        _rawModule: {} // 原始的options.modules.user配置对象
        get namespaced: true,
        _children: {}
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

根模块树

上文我们提到,runtime参数是用于标记区分动态注册和静态注册的,那么为什么要这样的区分呢?Vuex之所以提供动态注册的能力是为了提高Vuex的灵活性,比如一些第三方的插件可以通过动态注册在store实例中附加新的模块来增强或者使用Vuex的状态管理,如vuex-router-sync插件 (opens new window))。动态注册的模块可以被卸载,然而Vuex初始化时的模块不应该在运行时被卸载,这是确保Vuex状态管理的可维护性和一致性考虑。总的来说在保证Vuex的灵活性的同时,有以下的目的:

  • 保证一致性,简化状态管理的管理过程,使代码更加清晰,易于维护
  • 保证可读性和可预测性:使得状态树的结构在实例化时就被确定下来
  • 易于跟踪状态的变化,使其更好的与Vue开发者工具集成

# 3. 模块卸载unregister

如上文所说,Vuex是可以对动态注册的模块进行卸载的,unregister就是用来卸载动态模块的方法

// 卸载一个模块
unregister(path) {
  const parent = this.get(path.slice(0, -1)) // 获取父模块
  const key = path[path.length - 1] // 获取子模块的key
  if (!parent.getChild(key).runtime) return // 静态注册的不可卸载

  parent.removeChild(key) // 删除子模块
}
1
2
3
4
5
6
7
8

unregister方法接收一个参数,即要卸载的模块的路径,该路径为数组,如['user'],根据路径找到相应的父模块,如果获取到的模块是个动态注册的模块则调用父模块的removeChild方法将其从父模块中移除。

# 4. 更新模块

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

update方法用于更新整个模块树,接收一个参数,即要更新的模块配置对象。在update方法中会调用外部的update函数,update函数定义如下:

function update(path, targetModule, newModule) {
  if (process.env.NODE_ENV !== 'production') {
    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 (process.env.NODE_ENV !== 'production') {
          console.warn(
            `[vuex] trying to add a new module '${key}' on hot reloading, ` +
            'manual reload is needed'
          )
        }
        return
      }
      update(
        path.concat(key),
        targetModule.getChild(key),
        newModule.modules[key]
      )
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

与register方法类似,更新方法会先对新的模块进行校验,然后调用targetModule.update方法更新模块,如果新模块中有子模块且在原来的模块中有相应的定义,则递归调用update方法更新子模块。其中targetModule.update即是调用Module实例的update方法,有上文Module类的分析知道该方法会覆盖掉targetModule的getters、mutations、actions、namespaced等属性,而不会覆盖掉targetModule的state属性。

# 5. 其它实例方法

除了以上方法以外,ModuleCollection类还有get和getNamespace两个实例方法。

get方法定义如下:

get(path) {
  return path.reduce((module, key) => {
    return module.getChild(key)
  }, this.root)
}
1
2
3
4
5

其功能是接收一个数组类新的path路径,然后从this.root逐级往下查找模块,最终返回相应的模块。如

getNamespace方法定义如下:

getNamespace(path) {
  let module = this.root
  return path.reduce((namespace, key) => {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}
1
2
3
4
5
6
7

其功能是接收一个数组类新的path路径,然后从this.root逐级往下查找模块,如果查找的模块中有namespaced属性,则拼接上/,最后返回一个以/分隔的命名空间字符串。

如下例子:

this._modules = {
  root: {
    runtime: false,
    state: {}, // 原始的options.state
    _rawModule: {} // 原始的options配置对象
    get namespaced: false,
    _children: {
      user: {
        runtime: false,
        state: {}, // 原始的options.modules.user.state
        _rawModule: {} // 原始的options.modules.user配置对象
        get namespaced: true,
        _children: {}
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

调用get(['user'])会返回root._children.user,调用getNamespace(['user'])会返回user/。

# 6. 参数校验

在更新和注册模块的时候都会执行如下代码:

if (process.env.NODE_ENV !== 'production') {
  assertRawModule(path, rawModule)
}
1
2
3

assetRawModule方法对模块参数的合法性进行校验,相关函数定义如下:

// 函数断言配置
const functionAssert = {
  assert: value => typeof value === 'function',
  expected: 'function'
}

// 对象断言配置
// 函数或者含有类型为函数的handler属性的对象
const objectAssert = {
  assert: value => typeof value === 'function' ||
    (typeof value === 'object' && typeof value.handler === 'function'),
  expected: 'function or object with "handler" function'
}

// 断言的类型配置
const assertTypes = {
  getters: functionAssert,
  mutations: functionAssert,
  actions: objectAssert
}

// 断言rawModule是否合法
function assertRawModule(path, rawModule) {
  // 遍历getters,mutations,actions
  Object.keys(assertTypes).forEach(key => {
    if (!rawModule[key]) return

    // 获取断言配置
    const assertOptions = assertTypes[key]

    // 遍历getters,mutations,actions
    forEachValue(rawModule[key], (value, type) => {
      // 执行断言,不通过时抛出错误
      assert(
        assertOptions.assert(value),
        makeAssertionMessage(path, key, type, value, assertOptions.expected)
      )
    })
  })
}

// 根据传入的配置生成断言错误信息
function makeAssertionMessage(path, key, type, value, expected) {
  let buf = `${key} should be ${expected} but "${key}.${type}"`
  if (path.length > 0) {
    buf += ` in module "${path.join('.')}"`
  }
  buf += ` is ${JSON.stringify(value)}.`
  return buf
}

// utils.js
// 断言失败抛出异常
export function assert(condition, msg) {
  if (!condition) throw new Error(`[vuex] ${msg}`)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

其功能就是遍历相关参数,对getters、mutations、actions配置做类型校验,如果校验失败就抛出一个错误。基本的处理过程是:遍历模块的getters、mutations、actions属性,如果已经定义了,则再遍历其键值对,通过functionAssert、objectAssert定义的断言类型对配置的值进行断言,如果校验失败就抛出一个错误。

# 7. ModuleCollection总结

ModuleCollection类主要用来管理模块的注册、卸载和更新,其核心方法是register、unregister、update,其中register和unregister方法主要用来注册和卸载模块,update方法主要用来更新模块。ModuleCollection类还有get和getNamespace两个实例方法,get方法用于根据给定的路径获取模块,getNamespace方法用于更具给定的路径获取命名空间。

在new Store(options)中初始化_modules属性时,最终会得到一个包含root属性的ModuleCollection实例,options中所有的信息都会被记录到root属性中,用于后续安装根模块。

ModuleCollection类的基本结构如下图: vuex-module-collection-class

最近更新: 2024/06/27, 11:47
源码基本结构与install函数
Store实例化

← 源码基本结构与install函数 Store实例化→

最近更新
01
Axios源码解读
07-29
02
基于React16的Webpack升级与构建速度优化
07-09
03
Vue-Router源码解读
07-09
更多文章>
Theme by Vdoing | Copyright © 2024-2024 深海鱼 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式