Vuex 原理

Vue


Vuex 采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同:

  1. Vuex 的状态存储是响应式的
    • 当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。
  2. 不能直接改变 store 中的状态
    • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。

# 使用示例

整篇文章都会围绕着这个示例来进行解析,

  • moduleA 与 moduleB 分别为两个不同的模块,他们内部有各自的 state、getters、mutations
  • 初始示例中,两个模块的 state、getters、mutations 中定义的属性名称都相同
// moduleA
const moduleA = {
  state: {
    count: 0,
  },
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
  },
  mutations: {
    increment(state) {
      // 这里的 `state` 对象是模块 A 的局部状态
      state.count++
    },
  },
}

// moduleB
const moduleB = {
  state: {
    count: 0,
  },
  getters: {
    doubleCount(state) {
      return state.count * 2
    },
  },
  mutations: {
    increment(state) {
      // 这里的 `state` 对象是模块 B 的局部状态
      state.count++
    },
  },
}

let store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB,
  },
})

// 在组件中访问存储在 store 中的 state
this.$store.a.count // 0
this.$store.a.count // 0
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

接下来就通过上面这个示例,来一步步看 Vuex 的内部实现原理

# 原理解析

首先看下,在组件中为什么可以通过 this.$store 的方式来访问到存储在 store 中的状态?

# Store 实例的注入

Vuex 是通过 Vue 插件的形式被使用的,即Vue.use(Vuex),这一点跟 VueRouter 是一样的。而 Vue.use 方法内部会调用插件提供的 install 方法来完成插件的注册,下面就是 Vuex 的 install 方法实现:

let Vue
export function install(_Vue) {
  if (Vue && _Vue === Vue) {
    // ...
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}

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

  // Vue2.x版本
  if (version >= 2) {
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    // ...
  }

  function vuexInit() {
    const options = this.$options
    // 1. 根 Vue 实例
    if (options.store) {
      this.$store =
        typeof options.store === 'function' ? options.store() : options.store
    } else if (options.parent && options.parent.$store) {
      // 2. Vue 子组件实例通过 parent 属性来查找
      this.$store = options.parent.$store
    }
  }
}
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

跟 VueRouter 实例注入方式类似,Vuex Store 实例的注入也是通过 Vue.mixin 方法混入 Vue 的 beforeCreate 这个钩子来实现的,因为 beforeCreate 这个钩子的调用顺序是先父后子,所以子组件中可以通过 parent 属性来访问到根实例上的 $store 属性,这就实现了在任何Vue组件实例中通过 this.$store 的方式来访问到存储在store 中的状态

# Store 的实例化

class Store {
  constructor(options = {}) {
    // ...

    // ======== 1. store 实例属性初始化 ========= //
    this._committing = false // 是否正在执行commit的标记
    this._actions = Object.create(null) // 存储 actions
    this._mutations = Object.create(null) // 存储 mutations
    this._wrappedGetters = Object.create(null) // 存储被包装后的 getters

    this._modules = new ModuleCollection(options) // 对 module 的处理
    this._modulesNamespaceMap = Object.create(null) // 存储具有命名空间的 module 映射

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

    // ======== 2. 为commit & dispatch 方法绑定当前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)
    }

    let state = this._modules.root.state

    // ======== 3. modules 的注册 ======== //
    installModule(this, state, [], this._modules.root)

    // ======== 4. store._vm ======== //
    resetStoreVM(this, state)

    // ======== 5. 调用插件 ======== //
    plugins.forEach((plugin) => plugin(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

整个 Store 构造函数内的逻辑有很多,按照注释内容一步步看。

1. store 实例属性初始化中,有一个比较重要的部分,就是对于 modules 的处理,也就是 this._modules = new ModuleCollection(options) 的执行流程

# modules 的初始化

# ModuleCollection 的实例化

ModuleCollection 类的构造函数内部逻辑很简单,直接调用了 register 方法,进行了 modules 的注册工作。这里的 rawRootModule 就是例子中的实例化 Store 时传入的配置对象,

// prettier-ignore
class ModuleCollection {
  constructor(rawRootModule) {
    this.register([], rawRootModule, false)
  }

  // ====== 注册 modules ====== //
  register(path, rawModule, runtime = true) {
    // 这里又用到了 Module 类
    const newModule = new Module(rawModule, runtime)
    
    if (path.length === 0) { // ====== 1. 注册根 module ====== //
      this.root = newModule
    } else { // ====== 2. 注册子module ====== //
      // 根据path来获取父module
      const parent = this.get(path.slice(0, -1))
      // 将 子module 添加到 父module 的 _children 属性对象中
      parent.addChild(path[path.length - 1], newModule)
    }

    // 递归注册嵌套的子 modules
    if (rawModule.modules) {
      forEachValue(rawModule.modules, (rawChildModule, key) => {
        this.register(path.concat(key), rawChildModule, runtime)
      })
    }
  }
}

// forEachValue 方法
function forEachValue(obj, fn) {
  Object.keys(obj).forEach((key) => fn(obj[key], 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
30
31
32
33

register 方法的任务也很简单,就是完成 modules 的注册工作。

1、定义在 modules 中的每一个 module 都是 Module 类的实例,包括最顶层的 rawRootModule,且最顶层的 rawRootModule 实例会被赋值给 ModuleCollection 实例的 root 属性

2、这是一个递归的注册过程,如果某个 module 中又定义了 modules ,则会递归的去注册对应的子 module

3、子 module 会被注册到父 module 的 _children 属性中,这是通过 module 实例的 addChild 方法实现的

// @Module addChild 方法
Module.prototype.addChild = function addChild(key, module) {
  this._children[key] = module
}
1
2
3
4

4、父 module 的获取是通过 ModuleCollection 原型上的 get 方法实现的,

// @ModuleCollection get 方法
ModuleCollection.prototype.get = function get(path) {
  return path.reduce(function (module, key) {
    return module.getChild(key)
  }, this.root)
}
1
2
3
4
5
6

它的内部又调用了 Module 原型上的 getChild 方法

// @Module getChild 方法
Module.prototype.getChild = function getChild(key) {
  return this._children[key]
}
1
2
3
4

一定要记住这里的 module 注册是一个递归的过程,每一层的 module 都是一个 Module 类的实例

经过这里的递归注册之后,上面的例子最终构造出的 store 实例的 _modules 属性的值,差不多就是下面的形式:

{
  root: {
    _children: {
      a: {
        _children: {},
        _rawModule: {
          state: {
            count: 0,
          },
          getters: {
            doubleCountA: function doubleCountA(state) {
              return state.count * 2;
            },
          },
          mutations: {
            increment: function increment(state) {
              // 这里的 `state` 对象是模块 A 的局部状态
              state.count++;
            },
          },
        },
        state: {
          count: 0,
        },
      },
      b: {
        _children: {},
        _rawModule: {
          state: {
            count: 0,
          },
          getters: {
            doubleCount: function doubleCount(state) {
              return state.count * 2;
            },
          },
          mutations: {
            increment: function increment(state) {
              // 这里的 `state` 对象是模块 B 的局部状态
              state.count++;
            },
          },
        },
        state: {
          count: 0,
        },
      },
    },
    _rawModule: {
      modules: {
        a: {
          state: {
            count: 0,
          },
          getters: {
            doubleCountA: function doubleCountA(state) {
              return state.count * 2;
            },
          },
          mutations: {
            increment: function increment(state) {
              // 这里的 `state` 对象是模块 A 的局部状态
              state.count++;
            },
          },
        },
        b: {
          state: {
            count: 0,
          },
          getters: {
            doubleCount: function doubleCount(state) {
              return state.count * 2;
            },
          },
          mutations: {
            increment: function increment(state) {
              // 这里的 `state` 对象是模块 A 的局部状态
              state.count++;
            },
          },
        },
      },
    },
    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
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88

最顶层称为 root module,然后它有几个重要的属性,这几个属性在各层子孙 module 中都是一样的

  • _children: 每个 module 的子 moudle 会存储在 _children 中,他们都是 Module 类的实例
  • _rawModule: 定义在每个 module 中的原始 store 选项
  • state: 对应 module 的 state

接下来就看下 Module 类的实现

# Module 的实例化

  1. Module 类构造器内部初始化了一个 _rawModule 属性,用于存储原始的 module 属性值
  2. 还初始化了一个 state 属性,值为 options.state 的值或函数执行后的结果
  3. 另外还有一些遍历 getters、actions、mutations 的原型方法,这些方法后面会用到
class Module {
  constructor(rawModule, runtime) {
    // 用于存储上面提到的子 module
    this._children = Object.create(null)
    // 存储用户传入的原始 options 对象
    this._rawModule = rawModule
    const rawState = rawModule.state

    // 存储原始 module 的 state
    this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
  }

  // ...

  // 遍历子module
  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)
    }
  }
}
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

看到到这里,Vuex 针对 module 的处理流程就完成了,接下来就看 Vuex 是如何对初始化后的 module 进行注册的

# modules 的注册

这一步主要是通过 installModule 这个方法来实现的。这部分会涉及到两个比较重要的问题:

1、如何做到在不同 module 中定义的 action、mutation 和 getter 方法,接收的 state 参数是对应 module 的局部 state?

2、命名空间 (opens new window)namespaced 属性的设置对于 module 中 actions、mutations、getters 注册的影响

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

function installModule(store, rootState, path, module, hot) {
  // 如果path是空数组,则判定当前module为根module
  const isRoot = !path.length
  const namespace = store._modules.getNamespace(path)

  // ...

  // 对于非根module的处理
  if (!isRoot && !hot) {
    const parentState = getNestedState(rootState, path.slice(0, -1))
    const moduleName = path[path.length - 1]
    store._withCommit(function () {
      // set state
      Vue.set(parentState, moduleName, module.state)
    })
  }

  // 1. 创造一个上下文对象
  const local = (module.context = makeLocalContext(store, namespace, path))

  // 2. 注册 mutations & actions & getters
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })
  module.forEachAction((action, key) => {
    const type = action.root ? key : namespace + key
    const handler = action.handler || action
    registerAction(store, type, handler, local)
  })
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  // 3. 递归处理子 module
  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

首先来看第一个问题

# 局部 state

拿 getters 为例,它其实可以理解为 state 的计算属性,此外 getters 方法有两个参数,分别为 state 以及其他 getters。

而所谓的局部 state,就是在 moduleA 中定义的 getters,它的 state 参数就是定义在 moduleA 中的局部 state。这是如何做到的呢?

还是以文章开始部分的例子为例,并结合 installModule 方法来看一下这里的处理流程:

  1. 首先,由于例子中只在 rootModule 中定义了 modules,所以会走到递归处理子 module部分
module.forEachChild((child, key) => {
  installModule(store, rootState, path.concat(key), child, hot)
})
1
2
3

forEachChild方法在 Module 部分提到过,主要是用来遍历 module 实例的 _children 属性中存储的子 module,也就是例子中的 moduleA 和 moduleB,以 moduleA 为例

此时,传递给 installModule 方法的参数

  • path.concat(key) 就是 ['a'],
  • child 就是 moduleA 的 module 实例

这里正好顺带说一下 namespace 的获取,

const namespace = store._modules.getNamespace(path)

ModuleCollection.prototype.getNamespace = function getNamespace(path) {
  var module = this.root
  return path.reduce(function (namespace, key) {
    module = module.getChild(key)
    return namespace + (module.namespaced ? key + '/' : '')
  }, '')
}
1
2
3
4
5
6
7
8
9

由于例子中并没有为任何 module 设置 namespaced 属性,所以 module.namespaced 默认为 false,所以最终的 namespace 是一个空字符串。假设这里给 moduleA 设置了namespaced: true,那最终求得的 namespace 就会成为 a/

  1. 继续向下看,来到 const local = (module.context = makeLocalContext(store, namespace, path)),因为 namespace == '',所以下面的代码中省略了 namespace 不为空的处理逻辑
function makeLocalContext(store, namespace, path) {
  var noNamespace = namespace === ''

  var local = {
    dispatch: store.dispatch,
    commit: store.commit,
  }

  Object.defineProperties(local, {
    getters: {
      get: function () {
        return store.getters
      },
    },
    state: {
      get: function () {
        return 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

这个方法最终就是返回了一个 local 对象,这个对象会在后面的注册流程中当做参数传入给各个注册器。这里只需要关注该对象 state 属性的定义,

Object.defineProperties(local, {
  // ...
  state: {
    get: function () {
      return getNestedState(store.state, path)
    },
  },
})
1
2
3
4
5
6
7
8

可以看到 state 的值,是通过 getNestedState 这个方法获取的,该方法接收两个参数,

  • store.state:store 实例的 state 属性
  • path:对象的属性 path

这个属性的定义是在 Store 类中,

class Store {
  // ...

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

  set state(v) {
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `use store.replaceState() to explicit replace store state.`)
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

可以看到它实际是对 store._vm._data.$$state 的代理

const state = store._modules.root.state

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

getNestedState 这个方法就是实现局部 state 的关键

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

还是以 moduleA 为例,path = ['a'],所以通过 getNestedState 处理后,得到的 state 就是 store.state.a,即 moduleA 的 state

# namespace

关于命名空间的设置,前面也提到了一些,其实这个属性主要影响到的是上面的 const namespace = store._modules.getNamespace(path) 这行代码,非 namespaced 的 module,这个值始终是一个空字符 ''

以 moduleA 的 getters 为例,看看设置与不设置这个属性,最终的差异:

  1. 不设置
  1. 设置

所以,如果设置了 namespace 为 true,那么当想要访问 moduleA 中的 getters,就只能使用this.$store.getters['a/doubleCountA'],或者借助辅助函数,如:

...mapActions([
  'a/doubleCountA', // -> this['a/doubleCountA']()
])
1
2
3

这样的方式来访问了,具体可参见文档 (opens new window)

# 注册

还是以 getters 的注册为例,其实整体的过程很简单,主要是将传入的 getters 做一层包裹,并先缓存到了 store 实例的_wrappedGetters属性中

这里的 rawGetter 就是我们传入的 getter,可以看到它的参数就是在这里被注入的,包括局部 state、局部 getters 以及全局 state 和全局 getters

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

function registerGetter(store, type, rawGetter, local) {
  if (store._wrappedGetters[type]) {
    if (process.env.NODE_ENV !== 'production') {
      console.error('[vuex] duplicate getter key: ' + type)
    }
    return
  }
  store._wrappedGetters[type] = function wrappedGetter(store) {
    return rawGetter(
      local.state, // local state
      local.getters, // local getters
      store.state, // root state
      store.getters // root getters
    )
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

那我们实际在应用中访问的 getters 又是怎么定义的呢?

store.getters = {}
var wrappedGetters = store._wrappedGetters
forEachValue(wrappedGetters, function (fn, key) {
  Object.defineProperty(store.getters, key, {
    get: function () {
      return store._vm[key]
    },
    enumerable: true, // for local getters
  })
})
1
2
3
4
5
6
7
8
9
10

看到这里,有关 store 中各部分组件的注册以及初始化工作,基本上已经完成了。还有一个比较重要的问题,如何做到 store 中的 state 发生变化之后,主动触发视图的更新呢? 其实了解 Vue 双向绑定原理的话,大概也能猜出来需要怎么去做了:那就是将 state 变成响应式的就可以了。下面就看一下这一部分在 Vuex 中是怎么实现的

# 响应式 state

state 的响应式处理,其实也很简单,主要是借助了 Vue 实例的 data 是响应式的这一点

function resetStoreVM(store, state, hot) {
  // ...
  store._vm = new Vue({
    data: {
      $$state: state,
    },
  })
  // ...
}
1
2
3
4
5
6
7
8
9

再结合上面提到的 state 的定义

class Store {
  get state() {
    return this._vm._data.$$state
  }
}
1
2
3
4
5

# 如何避免用户直接修改 state

主要是利用了 Vue 实例的 $watch 方法,来监听 store._vm._data.$$state 属性的变化,只要第一个函数的返回值发生了变化,就会触发第二个回调函数的执行

function enableStrictMode(store) {
  store._vm.$watch(
    function () {
      return this._data.$$state
    },
    function () {
      if (process.env.NODE_ENV !== 'production') {
        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
16

而在回调函数内部,会有一个对 store._committing 属性的断言,这个值初始为 false,只有在调用_withCommit来变更 state 的时候,它才会置为 true,且变更成功后又会重置为原始值

Store.prototype._withCommit = function _withCommit(fn) {
  var committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}
1
2
3
4
5
6

所以原理就是通过深度监听 state 变化 + 判断 _committing 标志位来实现的

# 总结

其实还有其他的一些细节的东西没有说到,不过总体的流程基本上已经过了一遍。ok,就到这里~

Last Updated: 10/21/2024, 4:15:17 PM