深海鱼的博客 深海鱼的博客
首页
  • 《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实例化
      • 1. 构造函数
        • 1.1. 自动安装
        • 1.2. 非生产环境下的断言
        • 1.3. 初始化各种实例属性
        • 1.4. 增加绑定方法
        • 1.5. 安装根模块
        • 1.6. 响应式处理
        • 1.7. 安装插件
      • 2. installModule函数
        • 2.1 模块映射
        • 2.2 设置state
        • 2.3 注册mutation、action、getter
      • 3. resetStoreVM函数
        • 3.1 获取和定义定义相关属性
        • 3.2 getters代理
        • 3.3 state响应式处理
        • 3.4 开启严格模式
        • 3.5 销毁旧的store._vm实例
        • 3.6 小结
      • 4. 总结
    • Store实例方法
    • 辅助函数
    • 内置插件
    • 总结与常见问题
  • vue-router

  • vue3

  • react

  • Axios

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

Store实例化

Store类是Vuex的核心,它负责状态的管理、事件的派发与监听等工作。Store类定义在store.js文件中,如下:

// store.js
export class Store {
  constructor(options = {}) {
    // ...
  }
  //...
}
1
2
3
4
5
6
7

下面我们将结合例子,一步步的揭开Store类的面纱。

# 1. 构造函数

Store类的构造函数接收一个参数options,它是一个对象,包含以下属性:

  • state:状态对象,默认为空对象
  • actions:action对象,默认为空对象
  • mutations:mutations对象,默认为空对象
  • getters:getters对象,默认为空对象
  • modules:模块对象,默认为空对象
  • plugins:插件列表,默认为空数组
  • strict:是否为严格模式,默认为false

Store类构造函数的代码如下:

点击查看代码
  constructor(options = {}) {
    // 自动安装Vuex,确保在使用script脚本引入vuex时的自动安装
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      // 未安装就使用则提示需要安装
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      // 不支持Promise提示需要提供polyfill,因为vuex依赖Promise(actions)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      // 只能使用new操作符调用Store
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

    const {
      plugins = [], // 插件列表
      strict = false // 是否为严格模式
    } = options

    // 获取传入的state,如果为函数,则执行函数并返回结果
    // 否则直接返回state,为空时默认值是空对象
    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state() || {}
    }

    // 各种内部状态标识
    this._committing = false // 是否正在提交,用于mutation标记
    this._actions = Object.create(null) // 存放action是对象
    this._actionSubscribers = [] // 存放action订阅者
    this._mutations = Object.create(null) // 存放mutations对象
    this._wrappedGetters = Object.create(null) // 存放getters,用于计算属性
    this._modules = new ModuleCollection(options) // 创建根模块树
    this._modulesNamespaceMap = Object.create(null) // 模块命名控件映射
    this._subscribers = [] // 订阅列表
    this._watcherVM = new Vue() // 一个vue实例,用于触发watcher

    // bind commit and dispatch to self
    // 定义dispatch和commit为绑定函数, 将其this属性绑定至当前的store实例
    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)
    }

    // strict mode
    // 标记当前实例是否为严格模式
    this.strict = strict

    // 安装根模块
    // 1. 注册state、actions、mutations, getters
    // 2. 在_modulesNamespaceMap收集模块
    // 3. 递归安装子模块
    installModule(this, state, [], this._modules.root)
    // 将getters和state对象加入响应式系统
    // 将__wrappedGetters对象注册成计算属性,使用计算属性来实现其惰性求值的机制
    resetStoreVM(this, state)

    // apply plugins
    // 安装插件
    plugins.forEach(plugin => plugin(this))

    // 安装devtools插件
    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

# 1.1. 自动安装

if (!Vue && typeof window !== 'undefined' && window.Vue) {
  install(window.Vue)
}
1
2
3

当window存在时说明在浏览器的宿主环境中。当使用script标签引入Vuex和Vue时,会自动执行install方法。这个script为引入的使用提供了便利。#731 (opens new window)

# 1.2. 非生产环境下的断言

if (process.env.NODE_ENV !== 'production') {
  assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
  assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
  assert(this instanceof Store, `Store must be called with the new operator.`)
}
1
2
3
4
5
  • 如果未安装Vuex就使用new Store,则提示需要安装才可以使用
  • 如果宿主环境不支持Promise,则提示需要提供一个Promise的polyfill,这是因为Vuex的actions处理依赖Promise。
  • 如果没有使用new操作符调用Store,则提示需要使用new操作符。`

# 1.3. 初始化各种实例属性

const {
  plugins = [], // 插件列表
  strict = false // 是否为严格模式
} = options
// ...
this._committing = false // 是否正在提交,用于mutation标记
this._actions = Object.create(null) // 存放action的对象
this._actionSubscribers = [] // 存放action订阅者
this._mutations = Object.create(null) // 存放mutations对象
this._wrappedGetters = Object.create(null) // 存放getters计算属性
this._modules = new ModuleCollection(options) // 创建根模块树
this._modulesNamespaceMap = Object.create(null) // 模块与命名空间映射
this._subscribers = [] // 订阅列表
this._watcherVM = new Vue() // 一个vue实例,用于触发watcher
// ...
this.strict = strict

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  • _committing:是否正在提交,默认值为false,当该值被标记为true时,则state允许被修改,参考下文的this._withCommit方法。
  • _actions:存放所有action的对象,默认值为空对象。
  • _actionSubscribers:存放action订阅者,默认值为空数组,在使用dispatch派发action时,会遍历并触发订阅者,具体查看后文的dispatch方法。
  • _mutations:存放所有mutation的对象,默认值为空对象。
  • _wrappedGetters:存放所有getters的包装函数的对象,默认值为空对象。
  • _modules:根模块树,是Vuex模块化支持的基础,使用ModuleCollection类实例化,由上一篇《ModuleCollection与Module类》可知,其最终为一个具有一个root属性的模块树,后文的installModule方法会使用该模块树安装模块。
  • _modulesNamespaceMap:模块命名空间映射,定义的Vuex模块都会被映射到该对象的属性中,辅助函数的模块会从该对象获取。
  • _subscribers:mutation订阅列表,默认值为空数组,在使用commit提交mutation时,会遍历该数组并执行订阅者,具体查看下文的commit方法
  • _watcherVM:一个Vue实例,用于触发watcher,是store.watchAPI实现的基础,详见后文的watch实例方法
  • strict:是否开启严格模式,从options中获取,默认值为false。在严格模式下,任何在mutation处理函数以外修改Vuex state的操作都会抛出错误。

# 1.4. 增加绑定方法

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)
}
1
2
3
4
5
6
7
8

此处在Store实例增加了dispatch和commit两个绑定方法,这两个方法都是绑定到store实例上的。我们知道,dispatch和commit是可以在action中解构使用的,使用绑定函数,可以让它们的this始终指向store实例。如下:

new Vuex.Store({
  mutations: {
    INCREMENT (state) {
      state.count++
    }
  }
  actions: {
    increment ({ commit }) {
      commit("INCREMENT")
    }
  }
})

1
2
3
4
5
6
7
8
9
10
11
12
13

以上是Vuex在实例化时安装模块和响应式处理之前初始化的属性和方法,目前仅说明这些属性的基本作用,详细的内容需要依赖后文的解析。

# 1.5. 安装根模块

installModule(this, state, [], this._modules.root)
1

安装根模块调用了installModule函数,该函数接收当前store实例、state和根模块this._modules.root作为参数,其中:

(1)state由以下代码定义:

let {
  state = {}
} = options
if (typeof state === 'function') {
  state = state() || {}
}
1
2
3
4
5
6

即options.state可以是一个函数,如果函数的返回值不是falsy的函数则会将其结果作为state。state的默认值是一个空对象。

注意

Vuex并未对options.state的类型做任何处理,所以state可以是非falsy任何类型。但是无论在何种情况下,你应该保证state是一个对象。这是为了保证Vuex的响应式和模块系统的处理正确。

(2)this._modules.root由以下代码定义:

this._modules = new ModuleCollection(options) // 创建根模块树
1

由上一篇文章《ModuleCollection与Module类》可知,this._modules最终是一个具有一个root属性的模块树,root的结构和说明如下:

{
  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

执行installModule(this, state, [], this._modules.root)安装根模块时,会对this._modules.root的state、getters、mutations、actions进行处理,同时也会递归安装子模块,并与_modulesNamespaceMap做映射。关于根模块安装的细节,我们将在下文的installModule函数中讲解。

# 1.6. 响应式处理

resetStoreVM(this, state)
1

响应式处理主要是在store实例上添加_vm属性,该属性是一个Vue实例,并将state作为Vue实例的data属性,从而实现响应式。同时,也会对getters做处理,具体见下文的resetStoreVM函数的讲解。

# 1.7. 安装插件


const {
  plugins = [], // 插件列表
  strict = false 
} = options

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

// 安装devtools插件
if (Vue.config.devtools) {
  devtoolPlugin(this)
}
1
2
3
4
5
6
7
8
9
10
11
12

Store实例化时可以传入插件列表,每一个插件都是一个函数,会接收当前的store实例作为参数。一般我们会使用插件来订阅状态的变化,比如在Vuex中,我们可以使用vuex-persistedstate插件来将state持久化到localStorage中,也可以使用vuex-router-sync插件来将router和store进行同步。在默认情况下,如果浏览器安装了vue-devtools插件,Vuex会自动安装devtools插件,该插件会向devtools发送状态变化和action的日志,方便我们调试。

以下是一个简单的日志打印插件的示例:

function logger(store) {
  let prevState = JSON.stringify(store.state)
  store.subscribe((mutation, state) => {
    console.log(mutation.type)
    console.log('prev state: ' + prevState)
    console.log('next state: ' + JSON.stringify(state))
  })
}
1
2
3
4
5
6
7
8

以上就是Store类初始化的过程,初始化后,store会是一个结构上如下的对象:

{
  commit: function boundCommit(type, payload, options) {}, // commit绑定函数
  dispatch: function boundDispatch(type, payload) {}, // dispatch绑定函数
  strict: false, // 严格模式
  get state() {}, // state代理,待讲解
  getters: {}, // getters代理,待讲解
  _wrappedGetters: {}, // 被包装后的getters,待讲解
  _actions: {} // actions存储
  _actionSubscribers: [], // action订阅列表
  _mutations: {}, // mutations存储
  _subscribers: [], // mutation订阅列表
  _modules: ModuleCollection {root: Module {}}, // 模块树
  _modulesNamespaceMap: {}, // 模块命名空间映射
  _vm: Vue {}, // Vue实例,用于响应式处理
  _watcherVM: Vue {}, // Vue实例,用于触发watcher
  _committing: false, // 是否正在commit
  [[Prototype]]: Object // 原型上的一些实例方法
}

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

接下来我们来解析installModule函数和resetStoreVM函数。

# 2. installModule函数

函数定义:installModule(store, rootState, path, module, hot)

其接收5个参数的说明如下:

  • store:当前store实例
  • rootState:根state
  • path:当前模块的路径,数组形式,如['a', 'b']
  • module:当前模块的配置对象,即要安装的模块对象,里面含state,getters,mutations,actions等属性
  • hot:是否是热更新

函数的代码如下:

点击查看代码
function installModule(store, rootState, path, module, hot) {
  // 判断当前模块是否为根模块
  const isRoot = !path.length
  // 获取当前模块的命名空间
  const namespace = store._modules.getNamespace(path)

  // 带命名空间的模块在命名空间映射中登记模块
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // 设置state
  if (!isRoot && !hot) {  // 非根模块且非热更新时,设置模块状态
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(() => {
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 创建模块的局部上下文对象,用于访问局部状态和命名空间
  const local = module.context = makeLocalContext(store, namespace, path)

  // 注册模块的mutation
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key // 拼接命名空间
    registerMutation(store, namespacedType, mutation, local)
  })

  // 注册模块的action
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key // 转化key
    const handler = action.handler || action // 获取action的handler
    registerAction(store, type, handler, local)
  })

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

  // 遍历module._children,拼接命名空间,递归安装模块的子模块
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}

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

代码解析:

# 2.1 模块映射

const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
if (module.namespaced) {
  store._modulesNamespaceMap[namespace] = module
}
1
2
3
4
5

首先,根据path的长度标识当前模块是否为根模块,在Store构造函数中调用installModule时,path为空数组,所以isRoot为true,即代表安装根模块;接着令namespace为当前按安装模块的命名空间,getNamespace方法就是将['a', 'b']这样的path路径转为一个以/分隔的字符串,如a/b/,安装根模块时,path为[],所以namespace为空字符串;最后如果安装模块的namespaced属性为true,则会以namespace的值为键,将当前模块映射到_modulesNamespaceMap。换句话说,只有当模块的namespaced属性为true时,才会将当前模块映射到_modulesNamespaceMap中,否则其对应的actions、mutations都被注册到全局模块中,如下例子(后文会讲解注册过程):

new Vuex.Store({
  namespaced: true,
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  modules: {
    cart: {
      namespaced: true,
      list: [],
      mutations: {
        add(state, payload) {
          state.list.push(payload)
        }
      }
    },
    goods: {
      name: "apple",
      mutations: {
        add(state, payload) {
          state.name = payload
        }
      }
    }
  }
})
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

此时_modulesNamespaceMap和_mutations如下:

# 2.2 设置state

if (!isRoot && !hot) {  // 非根模块且非热更新时,设置模块状态
  const parentState = getNestedState(rootState, path.slice(0, -1))
  const moduleName = path[path.length - 1]
  store._withCommit(() => {
    Vue.set(parentState, moduleName, module.state)
  })
}
1
2
3
4
5
6
7

在非根模块的非热更新模式下,会设置当前模块的state:

  • 获取要安装的模块的父模块,通过getNestedState函数来获取
  • 往父模块里设置当前模块的state,通过Vue.set函数来设置,因为state的修改只能通过提交mutation来修改,所以这里通过_withCommit函数来提交一个mutation。

实例化Store时,安装的是根模块,所以不会执行以上逻辑。

getNestedState函数的函数定义如下,其功能是从根state开始,按照给定的路径找到对应的嵌套的state:

function getNestedState(state, path) {
  return path.length
    ? path.reduce((state, key) => state[key], state)
    : state
}
1
2
3
4
5

经过这一步的处理,我们不难知道,实例化Store时,如果传入的是一个包含模块的配置对象,那么state最终会是一个包含所有模块的state嵌套对象,如下:

const store = new Vuex.Store({
  state: {
    count: 0
  },
  modules: {
    a: {
      state: {
        name: "Foo"
      }
    },
    b: {
      state: {
        name: "Bar",
      },
      modules: {
        c: {
          state: {
            name: "Baz"
          }
        }
      }
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 2.3 注册mutation、action、getter

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

// 遍历module._rawModule.mutations,注册模块的mutation
module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key // 拼接命名空间
  registerMutation(store, namespacedType, mutation, local)
})

// 遍历module._rawModule.actions,注册模块的action
module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key // 转化key
  const handler = action.handler || action // 获取action的handler
  registerAction(store, type, handler, local)
})

// 遍历module._rawModule.getters,注册模块的getter
module.forEachGetter((getter, key) => {
  const namespacedType = namespace + key
  registerGetter(store, namespacedType, getter, local)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

首先通过makeLocalContext函数,创建一个局部上下文对象,用于访问局部状态和命名空间,并将其赋值给local和module.context,makeLocalContext函数的定义如下:

function makeLocalContext(store, namespace, path) {
  const noNamespace = namespace === '' // 没有命名空间

  // 局部的包含dispatch、commit、getters和state的对象
  const local = {
    // 如果没有命名空间则使用store的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("xxx", payload, {root: true}))
        type = namespace + type // 给type拼上命名空间
        // 未注册对应的action则提示错误
        if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
          return
        }
      }

      // 调用store的dispatch
      return store.dispatch(type, payload)
    },

    // 解析同dispatch
    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
      const args = unifyObjectStyle(_type, _payload, _options)
      const { payload, options } = args
      let { type } = args

      if (!options || !options.root) {// 不是根调用(dispatch("xxx", payload, {root: true}))
        type = namespace + type
        if (process.env.NODE_ENV !== 'production' && !store._mutations[type]) {
          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
          return
        }
      }

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

  // getters and state object must be gotten lazily
  // because they will be changed by vm update
  // 给local添加getters和state延迟获取的方法
  Object.defineProperties(local, {
    getters: {
      get: noNamespace
        ? () => store.getters // 无命名空间返回store的getters
        : () => makeLocalGetters(store, namespace)
    },
    state: {
      // 根据路径获取嵌套的state
      get: () => getNestedState(store.state, path)
    }
  })

  return local
}
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
58
59
60
61

makeLocalContext接收三个参数,分别是当前的Store实例store、命名空间namespace、路径path。其核心的逻辑是创建一个local对象并返回,该对象包含dispatch、commit、getters和state四个属性,这四个属性都会根据当前模块是否有命令空间来进行不同的处理。

  • dispatch:如果当前模块没有命名空间时,则直接使用store.dispatch,否则返回一个新的函数:
    • 统一不同调用风格的参数,通过调用unifyObjectStyle函数统一转为一个含{type, options,payload }属性的对象
    • 如果不是根调用(options.root为false),则给type拼上命名空间
    • 判断是否存在对应的action,如果不存在则提示错误
    • 调用store.dispatch(type, payload),此时的type可能是一个带了命名空间的字符串,这样就能找到对应的action
  • commit:和dispatch类似,只是最终调用的是store.commit
  • state:state被定义成了getter函数,这是因为state在后续的更新中可能会发生变化,所以需要延迟获取。如果当前模块没有命名空间,则直接返回store.state,否则通过getNestedState函数获取嵌套的state,并返回
  • getters:getters同样被定义成了getter函数,原因与state一致。如果当前模块没有命名空间,则直接返回store.getters,否则通过makeLocalGetters函数获取嵌套的getters并返回,makeLocalGetters函数的定义如下:
function makeLocalGetters(store, namespace) {
  const gettersProxy = {} // 定义一个getters代理对象

  // 获取命名空间长度
  const splitPos = namespace.length
  // 遍历全局的getters
  Object.keys(store.getters).forEach(type => {
    // skip if the target getter is not match this namespace
    // 检查当前getter是否匹配命名空间(以该命名空间开头)
    if (type.slice(0, splitPos) !== namespace) return

    // extract local getter type
    // 走到这里说明命名空间匹配
    // 提取命名空间之后的内容,作为代理属性
    const localType = type.slice(splitPos)

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

  // 返回代理的对象
  return gettersProxy
}
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

makeLocalGetters的核心逻辑就是创建一个代理对象gettersProxy,然后遍历store.getters的所有计算属性,根据命名空间进行过滤,将匹配的getter添加到gettersProxy中,并定义为getter属性,这样就实现了局部的getter代理访问。如下例子,local.getters.fullName最终会被代理至store.getters["user/fullName"]。

new Vuex.Store({
  state: {
    a: 1,
    b: 2
  },
  getters: {
    total: state => state.a + state.b
  },
  modules: {
    user: {
      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

回到local定义所在处,在定义完local这个局部的上下文后会分别遍历module._rawModule的mutations、actions、getters属性进行注册。上文中我们已经提到过,在调用new Store()时构造函数中会执行以下代码给store实例增加一个含有root属性的_modules属性:

this._modules = new ModuleCollection(options) // 创建根模块树
1

this._modules.root,其结构如下:

{
{
  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
19

(1)首先是遍历遍历_rawModule.mutations注册Mutation:

module.forEachMutation((mutation, key) => {
  const namespacedType = namespace + key // 拼接命名空间
  registerMutation(store, namespacedType, mutation, local)
})
1
2
3
4

registerMutation函数定义如下:

/**
 * 注册mutation
 * @param {Store} store store实例
 * @param {string} type mutation的type,可以是key,也可以是命名空间+key
 * @param {Function} handler 处理函数
 * @param {Object} local 局部上下文
 */
function registerMutation(store, type, handler, local) {
  const entry = store._mutations[type] || (store._mutations[type] = []) // 初始化
  // 添加处理函数
  entry.push(function wrappedMutationHandler(payload) {
    // 以store作为this,局部上下文的state和外部传入的payload作为参数调用处理函数
    handler.call(store, local.state, payload)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

遍历mutations后处理的过程是先通过命名空间与当前·mutation的名称计算要注册的mutation的type。然后将其处理函数包装后添加到store._mutations[type]中。包装后的函数会接收当前store、局部作用域下的state和payload作为参数。可以看到store._mutations[type]是一个数组,也就是说,同一个type可以注册多个mutation处理函数。如下例子:

const store = new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  modules: {
    inner: {
      state: {
        num: 0
      },
      mutations: {
        increment(state) {
          state.num++
        }
      }
    }
  }
})

store.commit('increment')
console.log(store.state)

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

执行后,里外两层的mutation.increment都会被调用,最后state的结果为:

{
  count: 1,
  inner: {
    num: 1
  }
}

1
2
3
4
5
6
7

(2)接着是遍历_rawModule.actions注册Action:

module.forEachAction((action, key) => {
  const type = action.root ? key : namespace + key // 转化key
  const handler = action.handler || action // 获取action的handler
  registerAction(store, type, handler, local)
})
1
2
3
4
5

registerAction定义如下:

/**
 * 注册Actions
 * @param {Store} store Store实例
 * @param {string} type action的type,可以是key,也可以是命名空间+key
 * @param {Function} handler 处理函数
 * @param {Object} local 局部上下文
 */
function registerAction(store, type, handler, local) {
  const entry = store._actions[type] || (store._actions[type] = []) // 初始化
  entry.push(function wrappedActionHandler(payload, cb) {
    // 以store绑定this,定义的上下文以及payload、cb作为参数调用处理函数
    let res = handler.call(store, {
      // 注入局部上下文的dispatch,commit,getters和state
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      // 注入根上下文的getters和state
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    // 返回结果不是一个thenable对象,则转为一个立即resolve的promise对象
    if (!isPromise(res)) {
      res = Promise.resolve(res)
    }
    // 安装了开发者工具插件
    if (store._devtoolHook) { // 触发devtools的hook
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}
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

actions也会处理其命名空间,跟mutations一样,同一个命名空间下的同名action也会被处理成数组。但与mutations相比,actions的处理有如下不同:

  • action处理函数除了接收局部的state外,还接收了局部的dispacth、commit和getters和state。同时还接收了根作用域下的rootGetters和rootState。这是因为在Vuex的设计中,mutation被设计成是同步的,它应该纯粹的用于操作当前上下文的state,而不应该被用于跨模块的操作。而action被设计成是异步的,在实际的处理逻辑中,可能会涉及多模块中间的数据处理。当然,实际的使用中也不是绝对的,但这是一种较好的实践。
  • action的返回结果会被处理成一个Promise对象,所以Vuex是依赖Promise才能正常工作的。

注意

从源码中,可以看到action的处理函数除了接收context、payload两个参数外,还接收了第三个参数cb,这查看Vuex官方的文档中并没有该参数的相关说明。查看Vuex的迭代记录,发现其在v3.1.2版本中,已经将cb参数移除。

(3)紧接着是遍历_rawModule.getters注册Getter:

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

registerGetter定义如下:

// 注册getters
function registerGetter(store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) { // 已经注册过了
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] duplicate getter key: ${type}`)
    }
    return
  }
  // getter收集
  store._wrappedGetters[type] = function wrappedGetter(store) {
    // 传入局部上下文的state、getters,全局的state、getters依次传入作为参数调用getter函数
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

可以看到,getters会将命名空间与key计算出type,然后通过store._wrappedGetters[type]收集起来,收集起来的函数是一个接收store实例参数,调用原始getter函数的包装函数,原始getter函数会依次接收局部上下文的state、getters,全局的store.state、store.getters作为参数调用。

(4)最后是递归安装子模块

到这里最外层的mutations、actions、getters已经注册完毕,如果当前模块有子模块,即module._children不为空,则递归安装子模块:

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

至此,installModule函数就执行完毕了。此时,store实例中的_mutations、_actions、_wrappedGetters、_modules、_modulesNamespaceMap等属性都已经被初始化完毕。如下例子:

点击查看代码
const store = new Vuex.Store({
  state: {
    firstName: 'Foo',
    lastName: 'Bar',
    count: 0,
  },
  getters: {
    fullName(state) {
      return state.firstName + ' ' + state.lastName
    }
  },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    AsyncIncrement(context) {
      context.commit('increment')
    }
  },
  modules: {
    inner: {
      namespaced: true,
      state: {
        num: 0,
        age: 18
      },
      getters: {
        person: (state, getters, rootState, rootGetters) => {
          return {
            firstName: rootState.firstName,
            lastName: rootState.lastName,
            name: rootGetters.fullName,
            age: state.age
          }
        }
      },
      mutations: {
        increment(state) {
          state.num++
        }
      },
      actions: {
        AsyncIncrement(context) {
          context.commit('increment')
        }
      },
    }
  }
})
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

结果:

vuex-installModule-finished

此时,我们还无法通过store.state和store.getters来获取相关的响应式值,因为它们还未被设置到store实例中。它们是通过resetStoreVM(store, state, hot)来实现的,接下来我们来分析resetStoreVM函数。

# 3. resetStoreVM函数

源码如下:

点击查看代码
function resetStoreVM(store, state, hot) {
  const oldVm = store._vm // 获取旧的store._vm实例

  // bind store public getters
  // 在store上添加公共属性getters,并将其代理到store._vm实例上
  // 这样就可以使用store.getters.xxx访问getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}
  // 遍历store._wrappedGetters,将其添加到computed上
  // 后续作为store._vm的计算属性传入
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    // 使用计算属性实现getters的缓存和惰性求值机制
    computed[key] = () => fn(store)
    // 代理getters的属性访问到store_vm实例上的属性
    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.config.silent = true
  // 添加store._vm实例,这是store响应式的核心
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  // 开启严格模式,严格模式下不能在mutation外部修改state
  if (store.strict) {
    enableStrictMode(store)
  }

  // 对旧的store._vm实例进行销毁
  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())
  }
}
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

# 3.1 获取和定义定义相关属性

const oldVm = store._vm 
store.getters = {}
const wrappedGetters = store._wrappedGetters
const computed = {}
1
2
3
4

首先会保存store._vm实例,这是为了后续可以销毁旧的store._vm实例,详细见后文详解。然后在store实例上添加getters属性,初始值为空对象,此操作也意味着如果store实例上原本有旧的getters属性,则会被覆盖。最后获取store._wrappedGetters属性并定义computed变量,用于后续代理getters。

# 3.2 getters代理

forEachValue(wrappedGetters, (fn, key) => {
  // use computed to leverage its lazy-caching mechanism
  // 使用计算属性实现getters的缓存和惰性求值机制
  computed[key] = () => fn(store)
  // 代理getters的属性访问到store_vm实例上的属性
  Object.defineProperty(store.getters, key, {
    get: () => store._vm[key],
    enumerable: true // for local getters
  })
})
1
2
3
4
5
6
7
8
9
10

通过遍历store._wrappedGetters,并将其添加到computed上面,从上文我们知道,每一个wrappedGetter的定义如下:

function wrappedGetter(store) {
  // 传入局部上下文的state、getters,全局的state、getters依次传入作为参数调用getter函数
  return rawGetter(
    local.state,
    local.getters,
    store.state,
    store.getters
  )
}
1
2
3
4
5
6
7
8
9

computed对应的函数中就会传入当前的store实例。computed在后续会被作为store._vm实例的计算属性传入。

之后,会将store.getters相关属性的访问代理至store._vm实例上,这样,store.getters.xxx就可以直接访问到store._vm实例上的xxx属性。

# 3.3 state响应式处理

const silent = Vue.config.silent
Vue.config.silent = true
store._vm = new Vue({
  data: {
    $$state: state
  },
  computed
})
Vue.config.silent = silent
1
2
3
4
5
6
7
8
9

这是Vuex响应式的核心,store._vm其实就是一个Vue实例,并将state作为data传入(data.$$state),同时将computed作为计算属性传入。也就是说,Vuex的getters最终会被转为Vue实例的计算属性,即getters是惰性求值的。

另外这里在定义store._vm实例时,会设置Vue.config.silent为true,即忽略Vue实例化时的警告信息。这样做的原因是因为:这个Vue实例是Vuex用来响应式处理state的,属于内部使用的实例,与用户业务上的Vue实例是不一样的。如果用户在全局定义了mixin(包括但不限于使用Vue.mixin和Vue.use定义),且假如这些mixin在Vue实例化的时候触发了警告,那么这些警告信息可能会干扰到开发者对Vuex本身的调试与理解,从而感到困惑。为了避免这些全局的mixin触发的警告带来的问题,这里就临时将Vue.config.silent设置为true,隐藏掉这些警告。

除了getters被代理到store._vm实例的计算属性上以外,所有对state的访问也会被代理到store._vm实例上。其定义在Store实例的访问器属性上:

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

所以对store.state的访问实际是对store._vm._data.$$state的访问,即前面传入resetStoreVM函数中传入的state。此state已经是处理过的具有嵌套结构的对象。

提示

这里我们还注意到了,store.state是代理到store._vm._data.$$state, 而不是store._vm.$$state,这是因为在Vue中,以$和_开头的属性被当作内部属性来看待,是不会被代理到Vue实例上的。

# 3.4 开启严格模式

如果设置了严格模式,则会开启严格模式的处理:通过同步深度监听state的变化,判断当前的操作是否处于commit操作中,如果不是,则抛出异常。

if (store.strict) {
  enableStrictMode(store)
}


function enableStrictMode(store) {
  // 同步深度监听_vm._data.$$state的变化
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      // 如果当前处于非commit状态,则抛出异常
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}

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

注意

尽管开启了严格模式,在直接修改state时,会抛出错误,但是此时state的修改仍然是响应式的,这些修改也会触发视图更新。

# 3.5 销毁旧的store._vm实例

Vuex是支持动态注册模块的,在动态注册模块的时候需要重新安装模块并更新响应式实例,所以需要销毁旧的store._vm实例,避免内存泄漏等问题。

const oldVm = store._vm 
// ...
store._vm = new Vue({
  // ...
})

if (oldVm) {
  if (hot) {
    // dispatch changes in all subscribed watchers
    // to force getter re-evaluation for hot reloading.
    // 分发所有订阅的watcher,强制重新计算getter,以强制触发热更新。
    store._withCommit(() => {
      oldVm._data.$$state = null
    })
  }
  // 销毁旧vm实例
  Vue.nextTick(() => oldVm.$destroy())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

在热重载时,通过将store._vm实例的state设置为null,可以触发所有订阅的watcher, 触发getter的重新计算,进而触发热更新。

将oldVm放到nextTick回调中销毁是因为:此时有可能还存在基于旧实例的响应式依赖(如计算属性、监听器和渲染函数等),为了确保在销毁 oldVm 之前,所有基于 oldVm 的响应式依赖都已经完成了它们的更新和清理工作,所以放到nextTick回调中销毁。

# 3.6 小结

resetStoreVM函数主要完成了以下几件事情:`

  • 为store添加_vm属性,其值为一个Vue实例
  • 为store添加getters属性,并将store._wrappedGetters转为计算属性传递给store._vm的computed选项
  • 将state作为store._vm的data.$$state选项传入
  • 通过以上处理将store.getters相关属性的访问代理至store._vm实例的计算属性上;结合store的访问器属性,将store.state代理值store._vm._data.$$state上
  • 严格模式的处理
  • 销毁旧的store._vm实例

# 4. 总结

通过以上处理,其实Store实例核心的初始化工作就已经全部完成了。其主要的工作就是根据传入的options配置,安装模块并实现响应式机制,然后对外提供一套API。执行new Store(options)的基本脉络如下图:

vuex-store-new

一个完整的Store实例实例化后结果如下:

示例代码
const store = new Vuex.Store({
  strict: true,
  state: {
    firstName: 'Foo',
    lastName: 'Bar',
  },
  getters: {
    fullName(state) {
      return state.firstName + ' ' + state.lastName
    }
  },
  mutations: {
    increment(state, payload) { }
  },
  actions: {
    AsyncIncrement(context) { }
  },
  modules: {
    inner1: {
      namespaced: true, // 开启命名空间
      state: {
        name: "inner1"
      },
      getters: {
        something(state, getters, rootState, rootGetters) {
          return state.hours * state.price
        }
      },
      mutations: {
        increment(state) { }
      },
      actions: {
        AsyncIncrement(context) {
          context.commit('increment')
        }
      },
    },
    inner2: {
      // 未开启命名空间
      state: {
        name: "inner2"
      },
      getters: {
        something(state, getters, rootState, rootGetters) {
          return state.hours * state.price
        }
      },
      mutations: {
        increment(state) { }
      },
      actions: {
        AsyncIncrement(context) {
          context.commit('increment')
        }
      },
    }
  }
})
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
58

vuex-store-instance.png

Vuex.Store实例提供一些实例方法并作为API供外部调用,下一篇文章我们来介绍这些实例方法。

最近更新: 2024/07/01, 18:59
ModuleCollection与Module类
Store实例方法

← ModuleCollection与Module类 Store实例方法→

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