Vuex 源码分析
假设你已经配置了 store 文件,并且执行了 Vue.use(Vuex),本文将从这里开始进行分析
Vue.use 的时候发生了什么?
vuex 默认导出了 install 方法,
// 省略了一些文件导入,只看关键部分
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
}
在执行 Vue.use 的时候其实就是在调用 install
export function install(_Vue) {
// 只允许一次vuex的注入
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
applyMixin(Vue) // 这里是什么呢?
}
applyMixin
紧接着调用了 applyMixin,这里是混入了一个生命周期 beforeCreate 的钩子,在这个生命周期里进行了 vuexInit
Vue.mixin({ beforeCreate: vuexInit }) // 将vuexInit注册到beforeCreate这个生命周期,在new Vue挂载后在
// 对应的生命周期中执行vuexInit,此时new Vuex.Store已经初始化完成
// 这里是 vuexInit 所做的事情
function vuexInit () {
const options = this.$options
// store injection
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store() // 如果new Vue传入的store参数是个函数则立即执行
: options.store
} else if (options.parent && options.parent.$store) {
// 保证每个实例化的vue都能通过 this.$store拿到store数据
this.$store = options.parent.$store
}
}
}
new Store
接下来分析我们 new Store 中的数据是怎么建立层级关系的,我们可能将 store 拆分成多个小的 store 然后在根 store 中通过 module 定义将小的 store 注册进来,那么他们通过什么建立联系的呢?
首先每个模块都会有一个自己的实例,定义在 module.js 中
class Module {
constructor(rawModule, runtime) {
this.runtime = runtime
// Store some children item
this._children = Object.create(null) // 保存子module的对象,用于和子module建立关系
// Store the origin module object which passed by programmer
this._rawModule = rawModule // 当前实例的module
const rawState = rawModule.state
// Store the origin module's state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
get namespaced() {
return !!this._rawModule.namespaced
}
// 省略一些其余方法定义
}
module-collection
module 实例的初始化以及通过命名空间建立索引定义在 module-collection.js 中,这里截取一个主要的方法
// path是根据命名空间形成的数组,默认为空数组,此时代表注册进来的为根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
this.root = newModule
} else {
// 从根module通过getChild一层一层找到当前module的父级
const parent = this.get(path.slice(0, -1))
// 与子module建立父子关系
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
if (rawModule.modules) {
// 如果当前注册进来的module还有子module,则递归注册
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
installModule
接下来是最关键的部分 installModule,这里是为什么不能通过其他方式修改 store 数据的原因
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
installModule(this, state, [], this._modules.root)
这个函数整体比较长,这里会在关键地方加一下注释
function installModule(store, rootState, path, module, hot) {
const isRoot = !path.length
// 这里获取根据module定义的名字拼接而成的命名空间
const namespace = store._modules.getNamespace(path)
// register in namespace map
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
if (!isRoot && !hot) {
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(
'.'
)}"`
)
}
}
// 这里将子module的state注册到父级state对象下
Vue.set(parentState, moduleName, module.state)
})
}
// 关键是这里,makeLocalContext对store中定义的数据和操作进行了进一步的约束和封装
const local = (module.context = makeLocalContext(store, namespace, path))
// 下面四个函数就是注册相应的事件
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)
})
module.forEachChild((child, key) => {
// 这里会对子module递归注册,因此会构建出每个makeLocalContext的值,进而对每个dispatch形成命名空间的嵌套
installModule(store, rootState, path.concat(key), child, hot)
})
}
makeLocalContext
首先看一下 makeLocalContext 做了哪些事情,这里挑关键点进行分析
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
if (!options || !options.root) {
// 首先是命名空间的拼接
type = namespace + type
if (process.env.NODE_ENV !== 'production' && !store._actions[type]) {
console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
return
}
}
// 这里是对dispatch的一层封装,实际上调用的是存在namespace的,在registerAction的时候会按照namespace将
// action注册到action的entry数组中,因此这里dispatch回去对应的数组中匹配
return store.dispatch(type, payload)
},
这里截取一个 registerAction 为例
function registerAction(store, type, handler, local) {
// 这里的type是已经包含命名空间的
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler(payload) {
let res = handler.call(
store,
{
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state,
},
payload
)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
if (store._devtoolHook) {
return res.catch((err) => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
resetStoreVM
通过以上操作我们已经知道 store 树的构建过程,以及每一级通过命名空间建立父子关系,那么 store 的数据是如何建立起响应式的呢?答案是 resetStoreVM 这个函数,我会在这个函数关键部分添加注释
function resetStoreVM(store, state, hot) {
const oldVm = store._vm
// bind store public getters
// 这里是外部可以挺过store.getters获取定义的getters的原因,因为在这里吧getters定义了
store.getters = {}
// reset local getters cache
store._makeLocalGettersCache = Object.create(null)
// 这里是从rawGetters拿到我们顶一个getter
const wrappedGetters = store._wrappedGetters
const computed = {}
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.
// 这个累了的key就是已经根据命名空间建立好层级关系的key
computed[key] = partial(fn, store)
// 这里是做了响应式的定义
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
// 定义一个vue实例,将计算属性和state放入,用于store._vm[key]可以响应式
store._vm = new Vue({
data: {
$$state: state,
},
computed,
})
Vue.config.silent = silent
// enable strict mode for new vm
// 这个函数主要是对非commit操作state的更改做出警告
if (store.strict) {
enableStrictMode(store)
}
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())
}
}