Vuex 源码解析
前言
本渣最近心血来潮学习了一下 Vuex 的源码,再次做一下整理和分享。
Vuex 作为一个专为 Vue.js 程序开发的状态管理模式,想必大家都很熟悉。那么大家在开发的过程中有没有想过遗下几个问题:
- Vuex 是怎么挂载到 Vue 上的呢
- Vuex 的数据响应式是如何实现的
- Vuex 的严格模式又是怎么回事
- 为什么 state 能够在所有子组件中使用 store
带着这些问题,我们一起看一下 Vuex 的源码:
这里首先上一张官网截图:
Vuex 挂载
vue 使用插件的方式只需要 Vue.use(plugin),对于 Vuex 就是Vue.use(Vuex).我们都知道 Vue 源码挂载插件部分,如果参数有 install 方法,则执行这个 install 方法。如果参数本身是一个 function,那么只会执行这个 function
那么我们下面看一下 Vuex 插件的 install 方法都做了一些什么事情:
store.js
export function install(_Vue) {
//避免重复安装
if (Vue && _Vue === Vue) {
if (process.env.NODE_ENV !== 'production') {
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
)
}
return
}
Vue = _Vue //存储Vue实例
//混入操作,主要是将vuexInit方法混入到Vue的beforeCreate钩子函数中
applyMixin(Vue)
}
从上面的代码,我们可以看出,install 方法接收一个 Vue 实例,然后先判断是否已经安装过 Vuex,后面存储该 Vue 实例,最后执行了一下混入操作。
然后我们来看一下这个混入操作:
mixin.js
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
//根据Vue的版本号来控制混入,2.0以上的就是用混入
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init ? [vuexInit].concat(options.init) : vuexInit
_init.call(this, options)
}
}
//init初始化函数,主要是挂载全局变量$store
function vuexInit() {
const options = this.$options
//如果当前节点为跟节点,则直接从options选项中获取store
if (options.store) {
this.$store =
typeof options.store === 'function' ? options.store() : options.store
//如果不是根节点,则通过options中的parent获取父组件的$store引用
} else if (options.parent && options.parent.$store) {
//子组件获取父组件的$store,每个子组件都能够获得到store对象
this.$store = options.parent.$store
}
}
}
该方法首先检测了一下 Vue 的版本,如果是 2.0 以上,则使用 Vue.mixin()方法,将初始化函数混入 Vue 的 beforeCreate 钩子函数中。如果 Vue2.0 以下的版本,则将初始化函数挂载到 Vue 原型的_init 属性上。
vueInit 函数主要是将 Store 实例挂载到 Vue 实例的$store 属性上。这里对是否是跟节点进行了判断,如果是根节点的话直接取值’options.store’,如果是组件(组件存在 options.parent 属性)的话,则取 options.parent.$store 即父组件的 Store。这样就保证了所有的子组件都能够取到相同的 Store。这样就保证了所有的子组件都能够取到相同的。
同时,这也解决了之前我们提到的问题:为什么 state 能够在所有子组件中使用 store。
我们都知道 options.store 即是Store的实例。所以,我们下面开始看看 Store 的构造函数到底做了什么事情:
Store
我们先看一下 store.js 中的 Store 的构造函数:
该构造函数首先判断是否已经安装了 Vuex,如果未安装则重新安装 Vuex 插件。然后检测当前环境。
//在浏览器环境下,如果插件还未安装,则会自动安装
if (!Vue && typeof window !== 'undefined' && window.Vue) {
//也就是说当Vue不生效时使用window下的Vue,他啥时候放上去的
install(window.Vue)
}
//判断在非生产环境:必须调用Vue进行注册,必须支持Promise,必须用过new 创建Store
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.`)
}
之后就是进行一系列的属性初始化操作,其中包含了是否开启严格模式,初始化 actions、mutations、getters 等属性。
这其中的重中之重就是 ModuleCollection,该属性的主要作用就是构造 module 对象,后面会详细分析
const {
plugins = [],
//是否开启Vuex的严格模式,在严格模式下,任何mutations函数以外的方式修改state都会抛出错误
strict = false,
} = options
// 用来判断严格模式下是否是用mutation修改state
this._committing = false
//actions,存放actions
this._actions = Object.create(null)
//action订阅函数
this._actionSubscribers = []
//mutations
this._mutations = Object.create(null)
//存放getter
this._wrappedGetters = Object.create(null)
//构建module对象
this._modules = new ModuleCollection(options)
//根据namespace存放module
this._modulesNamespaceMap = Object.create(null)
//存放订阅者
this._subscribers = []
//存储Vue实例,主要是使用其中的watch
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
初始化属性之后,接下来就是处理 commit 和 dispatch 方法了,这一步的主要目的是改变两个方法中的 this 指向,将其指向当前的 store。
const { dispatch, commit } = this
//下面两个方法都是改变dispatch/commit两个方法的this指向
this.dispatch = function boundDispatch(type, payload) {
return dispatch.call(store, type, payload)
}
//type:方法名,payload:载荷 options:配置项
this.commit = function boundCommit(type, payload, options) {
return commit.call(store, type, payload, options)
}
下面主要是模块化的处理、数据响应式的处理、插件注册等一系列的操作。下面我们来仔细看一下:
// 是否开启严格模式
this.strict = strict
//获取根模块state
const state = this._modules.root.state
//只有根模块才有root属性
//模块化处理,递归的处理所有的子模块
installModule(this, state, [], this._modules.root)
//通过使用vue实例,初始化store._vm,使state变成可响应的,并且将getters变成计算属性(代理属性)
resetStoreVM(this, state)
//注册插件
plugins.forEach((plugin) => plugin(this))
//调试工具注册
const useDevtools =
options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
至此,就是 store 构造函数全部内容了,其主要操作就是初始化各种属性,模块化的处理,数据响应式的处理等一些操作。
下面我们先来看一下模块化的实现
模块化
什么是模块化,这里引用官网上的一段话:
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
由 store 构造函数我们可知,module 的初始化是由 ModuleCollection 完成的,那么我们接下来看一下 ModuleCollection 都做了些什么事情
module-collection.js
其构造函数主要是注册根模块
//注册根module
register(path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
assertRawModule(path, rawModule)
}
//调用Module的构造函数
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
//将module挂载根root上
this.root = newModule
} else {
//注册子module,将子module添加到父module的_children属性上
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
//注册嵌套模块,如果有子modules,循环注册
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
关于注册函数的主要内容就是构造 Module 实例化对象,然后递归处理子模块,以确保我们可以通过 store.state.rawChildModule 获取子模块的 state 状态。
接下来我们看一下 Module 的内容
module.js
export default class Module {
constructor(rawModule, runtime) {
//初始值为false
this.runtime = runtime
//存储子模块
this._children = Object.create(null)
//将原来的module存储,以备后续使用
this._rawModule = rawModule
const rawState = rawModule.state
//存储原来module的state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
addChild() {}
removeChild() {}
getChild() {}
update() {}
//...
}
Module 的内容其实很简单,主要就是提供了个中操作 module 的各种方法。
至此,我们理清了 ModuleCollection 的大致功能,就是接收我们创建 Store 传入的 options 选项,然后使用 options 构造一个 Module 对象,同时递归处理。最终构建一个 Module 树。
Module 树构建完成以后,我们继续看 Store 的构造函数。这个时候通过installModule方法处理该 Module 树,installModule 的函数比较长,这里只粘贴部分代码:
function installModule(store, rootState, path, module, hot) {
//是否是根module,根节点的path=[]
const isRoot = !path.length
//获取module的namespace
const namespace = store._modules.getNamespace(path)
//如果有namespace,则在_moduleNamespaceMap中注册
if (module.namespaced) {
if (
store._modulesNamespaceMap[namespace] &&
process.env.NODE_ENV !== 'production'
) {
console.error(
`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join(
'/'
)}`
)
}
store._modulesNamespaceMap[namespace] = module
}
// set state
//不是根节点,把子组件的每一个state设置到其父级的state属性上
if (!isRoot && !hot) {
//获取父级的state
const parentState = getNestedState(rootState, path.slice(0, -1))
//获取当前module的名字
const moduleName = path[path.length - 1]
store._withCommit(() => {
if (process.env.NODE_ENV !== 'production') {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join(
'.'
)}"`
)
}
}
//通过Vue.set()方法将当前的module的state挂载到父state上,通过Vue将子module设置成响应式
Vue.set(parentState, moduleName, module.state)
})
}
//给context对象赋值,设置局部的dispatch、commit方法以及getters和state
const local = (module.context = makeLocalContext(store, namespace, path))
//遍历注册mutation
module.forEachMutation()
//遍历注册action
module.forEachAction()
//遍历注册getter
module.forEachGetter()
//递归处理module
module.forEachChild()
}
以上便是 installModule 方法的大致内容了。我们可以看到,installModule 函数先判断了当前节点是否是根节点以及是否设置了命名空间 namespace。如果是根节点则将 module 挂载到 store._modulesNamespaceMap 上,否则将子组件的 state 挂载到到父级的 state 属性上。
这步主要是通过 Vue.set 来完成。然后通过 makeLocalContext 方法将 dispatch、commit、getters 和 state 挂载到 module 的 context 属性上,这个过程也是递归处理的。
然后我们退回之前的 Store 构造函数的后续操作resetStoreVM()
响应式
我们都知道 Vuex 中的数据都是响应式的,而 resetStoreVM()的主要功能就是实现数据的响应化。下面我们来看一下 resetStoreVM 的内容
store.js
//存放旧的Vue实例对象,该实例对象的主要作用就是实现state和computed的响应化
const oldVm = store._vm
//初始化Store中的getters
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
//通过Object.defineProperty的数据拦截功能,为每一个getter设置get方法,其实就是数据代理
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
computed[key] = partial(fn, store)
//在getters上添加属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true, // for local getters
})
})
由上面的代码我们可以看出,restoreVM 首先初始化 store 实例的 getters,然后利用 Object.defineProperty 的数据拦截功能实现数据代理,这样我们就可以通过 store.getters 访问 getters 里面的内容了。
除了数据代理之外,我们来看一下后面的内容:
const silent = Vue.config.silent
//暂时设置为true的目的是在new一个Vue实例的过程中不会报出警告
Vue.config.silent = true
//new一个Vue实例对象,运用Vue内部的响应式实现注册state以及computed,所以Vuex和Vue是强绑定的
store._vm = new Vue({
data: {
$$state: state,
},
computed,
})
Vue.config.silent = silent
//使用严格模式,保证修改store只能通过mutation
if (store.strict) {
enableStrictMode(store)
}
//如果旧的vm存在
if (oldVm) {
//解除旧的vm的state饮用,以及销毁旧的Vue对象
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
从上面的代码中我们可以看出,后续操作主要是通过 Vue 实现 state 和 computed 的响应化,然后接着判断是会否开启严格模式。最后,如果存在旧的 Vue 实例,我们还要通过 Vue.nextTick()方法销毁旧的 Vue 实例。
总结一下 resetStoreVM()方法,主要是实现了 getter 的代理、state 和 computed 的数据响应化。同时,他也判断了是否开启严格模式。下面我们来看一下严格模式的具体实现吧:enableStrictMode(store).
严格模式
借用 Vuex 官网上的一段话:
在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
那么我们接下来看看,他的严格模式的跟踪到底是怎么实现的:
store.js
function enableStrictMode(store) {
store._vm.$watch(
function () {
return this._data.$$state
},
() => {
if (process.env.NODE_ENV !== 'production') {
//断言检测,检查store中的_committing的值,如果是true则代表不是通过mutation方法修改的
assert(
store._committing,
`do not mutate vuex store state outside mutation handlers.`
)
}
},
{ deep: true, sync: true }
)
}
enableStrictMode()方法的内容很简单,其实就是接收一个 Store 实例,然后利用 Store 实例上挂载的 Vue 实现对 state 数据的监听。这里的监听主要是判断 store._committing,如果为 false 则抛出错误告诉用户需要使用 mutations 来修改数据。
store._committing 我们都知道是在 Store 的构造函数中初始化的,那么我们来看一下 store._committing 是在什么地方被标记的:
//store中的commit方法,执行mutation语句
_withCommit(fn) {
//保存之前的提交状态
const committing = this._committing
//进行本次提交,如不设置为true,直接修改state,strict模式下,Vuex将会产生非法修改state的警告
this._committing = true
fn()
//修改完成后还原本次修改之前的状态
this._committing = committing
}
}
其实该方法就是 store 中的 commit 方法,方法很简单。他最终执行的是 传入的 mutation 语句。这里会将_committing 设置为了 true。
总结一下 Vuex 的严格模式,就是在 Store 初始化的时候设置了一个标识用来判断是否是 mutation 提交的数据。在严格模式下,通过Vue.watch来监听数据的变化,如果不是 mutations 提交的数据则抛出错误。
commit(mutation) 和 dispatch(action)
commit
众所周知,commit 的主要作用就是提交 mutation,那么我们来看一下 commit 都做了一些什么事情,这里我们只看部分比较重要的代码:
store.js
//commit函数先进行适配处理,然后判断当前action type是否存在,如果存在则调用_withCommit 函数执行相应的mutation
commit(_type, _payload, _options) {
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
//取出type对应的mutation方法
const entry = this._mutations[type]
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
//执行mutation中的所有方法
this._withCommit(() => {
entry.forEach(function commitIterator(handler) {
handler(payload)
})
})
//通知所有订阅者
this._subscribers(...)
}
可以看出 commit 的主要作用就是提调用_withCommit()方法提交 mutation。这里的_withCommit 就是我们之前提到的那个_withCommit 方法。
dispatch
dispatch 的作用是调用 action。他的原理和 commit 是相同的,唯一不同的地方就在于因为action(所以 action 能够执行异步)是一个 Promise,所以 dispatch 需要调用 Promise.all 方法来执行所有的 action 函数。
这里我们只粘贴 dispatch 相较于 commit 不同的代码:
store.js
try {
this._actionSubscribers
.slice()
.filter((sub) => sub.before)
.forEach((sub) => sub.before(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
//是数组则包装Promise形成一个新的Promise,只有一个则返回第0个
const result =
entry.length > 1
? Promise.all(entry.map((handler) => handler(payload)))
: entry[0](payload)
//执行最后的Promise
return result.then((res) => {
try {
this._actionSubscribers
.filter((sub) => sub.after)
.forEach((sub) => sub.after(action, this.state))
} catch (e) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
return res
})
工具函数
我们平常所用的一些 mapState、mapAction、mapGetters 等哦那工具函数均是在 helper.js 文件中实现的。
这些方法的实现都是大同小异的,简单来说都是接收传入的 namespace,然后调用 store._modulesNamespaceMap 映射,获取相应的 state、actions 等数据。
因为方法很简单,这里只是简单的介绍一下,就不粘贴代码了。
做一个自己的 Vuex
Vuex 的代码还是比较简单的,代码量也很少。我们完全可以结合 Vuex 官网,将其作为一个文档来看。通过对 Vuex 源码的解析,我们完全可以实现一个简单的 Vuex 框架,以便加深我们对 Vuex 框架的理解。
这里附上写的简单的 Vuex 框架的地址: