vuex作为vue官方出品的状态管理插件,以及其简单API设计、便捷的开发工具支持,在vue项目中得到很好的应用。
了解其底层实现原理有助于我们更好的项目开发,理解与运用。
我们知道,vuex是作为vue框架的一个插件而存在,vuex只能使用在vue上,很大的程度是因为其高度依赖于vue的响应式系统以及其插件系统。每一个vue插件都需要有一个公开的install方法,vuex也不例外。
我们首先查看Vuex的入口文件src/index.js
源码:
// src/index.js
# 导出一个store插件对象
export default {
Store, # store类对象
install, # 安装方法
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
可以看到导出的store插件对象里面包含了两个最重要的内容:
- Store类。
- install方法。
1,Store 类
我们平时在项目中使用Vuex:
import Vue from 'vue'
# 这个vuex就是上面src/index.js导出的默认对象,里面包含了Store和Install
import Vuex from 'vuex'
// 注册vuex插件 调用Install
Vue.use(Vuex)
# 初始化一个Store实例 参数为选项对象
export default new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
},
plugins: []
})
我们继续看Store类的定义:
// src/store.js
# store类的定义
export class Store {
# 主要看构造器:参数为选项对象
constructor (options = {}) {
# 从参数对象中取出plugins插件列表,默认为一个空数组
const {plugins = [],strict = false} = options
// store internal state
# 初始化store上的一些实例属性
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// bind commit and dispatch to self
# 将commit和dispatch方法绑定为正确的调用
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)
}
// 严格模式
this.strict = strict
// 取出state对象数据
const state = this._modules.root.state
# 重点:创建响应式的state
resetStoreVM(this, state)
# 循环注册插件
plugins.forEach(plugin => plugin(this))
# 启用devtool
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
# 定义state访问器属性
get state () {
return this._vm._data.$$state
}
set state (v) {
if (__DEV__) {
assert(false, `use store.replaceState() to explicit replace store state.`)
}
}
# 同步提交方法,立即生效
commit (_type, _payload, _options) {
// check object-style commit
...
}
# 异步提交方法,需要等待Promsie状态确定后才修改state数据,所以我们使用数据需要根据Promise来操作
dispatch (_type, _payload) {
// check object-style dispatch
...
}
subscribe (fn, options) {
return genericSubscribe(fn, this._subscribers, options)
}
subscribeAction (fn, options) {
const subs = typeof fn === 'function' ? { before: fn } : fn
return genericSubscribe(subs, this._actionSubscribers, options)
}
watch (getter, cb, options) {
if (__DEV__) {
assert(typeof getter === 'function', `store.watch only accepts a function.`)
}
return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}
}
可以看见class Store
里面定义了很多的内容,比如我们最熟悉的commit和dispatch
方法。所以我们在通过new Store
初始化一个Store实例后,可以通过store.commit()
来操作state数据。
然后我们重点看constructor
构造器的内容,这里面的内容虽然比较多,但实际上只有三个重点:
- 初始化一些实例属性。
- 创建响应式的state【重点】。
- 循环安装插件【比如数据持久化插件:将存储在内存的state数据同步到sessionStorage】。
我们重点看对state的处理:
resetStoreVM(this, state)
// src/store.js
function resetStoreVM (store, state, hot) {
...
// 创建了一个vue实例存储到_vm属性
# 本质就是创建了一个隐藏的Vue组件,因为vue2的Vue实例可以作为组件来使用,Vue3的createApp不行
store._vm = new Vue({
# state 作为组件的data
data: {
$$state: state
}
})
}
省略一些边缘代码后,我们直接看resetStoreVM
方法核心内容,为store对象定义了一个_vm
属性,这个属性值为一个Vue实例。熟悉Vue2的源码都知道,vue2的组件构造器继承自Vue构造器,所以Vue可以当成一个组件来使用,它也可以传入data选项,来创建响应式的数据【只是平时我们的项目并不需要这样使用】,这里在data中只定义了一个响应式数据$$state
,它的值就是我们传入的state对象。
所以resetStoreVM
函数最重要的作用就是:给store对象定义了一个_vm
属性来存储响应式的state数据。
我们再回头看看Store类中定义的访问器属性state:
# 定义state访问器属性
get state () {
// 实际访问
return this._vm._data.$$state
}
所以我们在组件中使用this.$store.state实际上访问的就是响应式的store._vm._data.$$state
。
注意: 这里为啥是
_vm._data.$$state
,而不是直接访问_vm.$$state
,熟悉Vue2源码响应式原理的可以知道,组件的响应式数据真正是存储在_data
属性上的。我们在组件中可以直接通过this.count访问变量,是因为组件的data选项初始化时创建了对应的访问器属性count,代理到了_data
属性,即this.count
等同于访问this._data.count
。所以上面也是同理。
我们现在已经知道了new Vuex.Store()
初始化后Store实例的内容了:它是一个对象,存储了响应式的state数据,以及操作sate数据的方法。注意: 但是我们为啥能够在每个组件上使用this.$store来访问store里面的内容呢?这就和Install方法有关了。
2,install 方法
我们查看install
源码:
// src/store.js
export function install (_Vue) {
...
# 调用混入
applyMixin(Vue)
}
我们继续查看applyMixin
源码:
// src/mixins.js
# 对应applyMixin方法
export default function (Vue) {
// 获取vue的版本
const version = Number(Vue.version.split('.')[0])
// 如果版本大于2,
if (version >= 2) {
# 则使用Vue.mixin混入一个全局对象
// 注意:一个混入对象可以包含任意组件选项,这里为每个组件混入了beforeCreate生命钩子函数
Vue.mixin({ beforeCreate: vuexInit })
} else {
// for 1.x backwards compatibility.
...
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
# vuex初始化方法
function vuexInit () {
const options = this.$options
// 对每个组件实例注入$store属性 即store对象
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store
}
}
}
applyMixin
方法内容比较简单,主要就是使用Vue.mixin
全局混入了一个对象,即为每个组件混入了beforeCreate钩子函数。
注意: 当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
1,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
2,同名钩子函数将合并为一个数组,因此都将被调用。并且混入对象的钩子将在组件自身钩子之前调用。
这个钩子函数的内容就是vuexInit
方法,而它的内容其实就是为当前组件实例设置一个$store
属性。所以每个组件在初始化时,其组件实例都被注入了一个$store
属性【这个属性优先会从自身的$options上取值,如果没有就会从父级上取值】,而最开始的注入就是从根组件开始:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
new Vue({
router,
store, # 将store对象注入到根组件
render: h => h(App)
}).$mount('#app')
最终通过install方法:将 store 对象从根组件中注入到所有的子组件里,这也是为什么每个组件都可以使用this.$store来获取store对象的数据。
最后一句话总结Vuex原理: 通过install方法为每个组件混入了beforeCreate生命周期钩子函数,函数的内容就是为当前组件实例设置一个$store
属性,值为new Vue.Store
生成的store对象,这样在每个组件内都可以通过this.$store访问state数据。
根据Vuex源码可以推测vue-router插件使用了相同的原理。