vuex是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension
,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。(Vuex官网)
提示
- 本文只分析主要代码(主流程代码)
- 阅读本文的前提是至少使用过
vuex
- 源码 版本是 3.x 版本 (截止到2021/04/23)
- 本文的主要目的是记录一下自己的学习过程
目标
- 了解Vuex的安装过程
- 了解Store的实例化过程
先看一个简单的 vuex
是初始化的过程
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
count: 1
},
getter: {
count: state => state.count
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
return Promise((resolve, reject) => {
setTimeout(() => {
context.commit('increment')
resolve(context.state)
}, 1000)
})
}
},
modules: {
a: {
state: {
aCount: 1
}
},
b: {
state: {
bCount: 1
}
}
}
})
new Vue({
el: '#root',
router,
store,
render: h => h(App)
})
复制代码
Vuex的安装过程
首先 vuex
的安装方法(install)在 src/store.js 中
// src/store.js
export function install (_Vue) {
Vue = _Vue
applyMixin(Vue)
}
// src/mixin.js
function applyMixin (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
...
//这里是 vuex1.x版本的代码,可以忽视
}
function vuexInit () {
const options = this.$options
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
}
}
}
复制代码
可以看到 vuex
的安装过程还是比较简单的 暴露一个 install
方法,在调用 Vue.use(Vuex)
的时候会调用 install
方法。install方法就是在Vue中mixin一个beforeCreate钩子函数,beforeCreate中干的事就是把通过 new Vuex.Store()
生成的实例绑定到 Vue的每一个实例上 的this.$sotre
,这样我们在组件中就可以通过 this.$store
访问 vuex 的实例。
Store的实例化过程
store
就是一个数据仓库,为了更方便的管理仓库, 我们把一个大的 store
拆成一些 modules
,整 个 modules
是一个树型结构。每个 module
又分別定义了 state
, getters
, mutations
、 actions
我们也通过递归遍历模块的方式都完成了它们的初始化。
还是在 src/store.js 中,vuex
暴露了一个 Store
的构造函数。先看 constructor()
,原型方法在执行到的时候再看。
export class Store {
constructor (options = {}) {
//...
const {
plugins = [],
strict = false
} = options
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)
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
const state = this._modules.root.state
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
plugins.forEach(plugin => plugin(this))
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
// 下面是原型方法
// ...
}
复制代码
首先是定义了一大堆初始化变量,大多数我们可以先不看,等到用到的时候再回头看,先看执行代码(干了事的代码)。
分析执行的代码我们可以得到在 constructor()
中主要执行了下面几个方法,其实也就是整个Store
整个初始化的过程中几个主要的过程。
new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
下面我们分别进入这几个方法具体分析一下。
new ModuleCollection
代码位置 src/module/module-collection.js
根据构造函数名我们大致可以得出这是初始化store
中modules
的构造函数,带着这个思路我们还是先看 constructor()
。
export default class ModuleCollection {
// rawRootModule 就是我们初始化时传入 new Vuex.Store(optionss) 的 options
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
}
复制代码
constructor()
很简单,就执行了一个 this.register([], rawRootModule, false)。接着看 register
方法。
rawModule
就是我们传入的 options 或者 options下的 module
,也就是用于生成 module
的原始数据。
register (path, rawModule, runtime = true) {
// 初始化 module
const newModule = new Module(rawModule, runtime)
// 第一次进来 path为空 register 中第一个参数为 []
if (path.length === 0) {
// 将通过第一次传入的 options 生成的 modeule 作为 root 绑定到 this 上,方便取 root module
this.root = newModule
} else {
// 之后再进来 path 就有值了 取出来 root module,再次生成的 module 都会绑定到 root module 中
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// 如果 我们初始化的数据中有 modules 就继续递归执行 register
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
// 代码位置 src/module/module.js
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// 初始化子 module,当前 module 下的子 rawModule 都添加到 _children 中
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
// 将moudle中的 state 绑定到 this 上
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
复制代码
这样我们就能生成一个 modules
的树状数据,并把数据放到 this._modules 中。
modules
大概是这个样子,可以看到 modules
目前只是生成了一个树状结构和初始化了每个module
的state
,接下来我们继续看代码。
// rowModule 就是该module的原始数据
{
root: {
state: { count: 1 },
_children: {
a: {
state: { aCount: 1},
_children: {}
_rawModule: rowModule
},
b: {
state: { bCount: 1},
_children: {}
_rawModule: rowModule
}
},
_rawModule: rowModule
}
}
复制代码
installModule
installModule 是初始化 module
中 getter
mutations
actions
。
直接看代码
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
// 获取当前module 的 namespace,root module的namespace为 ""。
const namespace = store._modules.getNamespace(path)
// register in namespace map
if (module.namespaced) {
if (store._modulesNamespaceMap[namespace] && __DEV__) {
console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
}
// 记录namespace和 module 的对应关系
// {
// 'a/': aModule,
// 'b/': bModule
// }
store._modulesNamespaceMap[namespace] = module
}
// 这里可以先不看,和响应式数据有关的
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
if (__DEV__) {
if (moduleName in parentState) {
console.warn(
`[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
)
}
}
Vue.set(parentState, moduleName, module.state)
})
}
const local = module.context = makeLocalContext(store, namespace, path)
// 将 mutation 循环绑定到当前module上
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 将 action 循环绑定到当前module上
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 将 getter 循环绑定到当前module上
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 如果改 module 下有子module 递归继续执行 installModule
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
复制代码
上面的核心代码:
-
const local = module.context = makeLocalContext(store, namespace, path)
-
registerMutation(store, namespacedType, mutation, local)
-
registerAction(store, type, handler, local)
-
registerGetter(store, namespacedType, getter, local)
makeLocalContext
// 我直接把非关键代码直接删除了
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
// unifyObjectStyle 是统一参数的方法,这个方法让 dispatch 和 commit 可以采用两种不同的入参方式
// commit('increment', 1) | commit({type: 'increment', payload: 1})
// 这里的dispatch
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
return store.dispatch(type, payload)
},
commit: noNamespace ? store.commit : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
store.commit(type, payload, options)
}
}
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
复制代码
可以看到 makeLocalContext 返回了一个 local对象,local对象的目的是为该module
设置局部的 dispatch
、commit
方法以及getters
和state
(由于namespace
的存在需要做兼容处理)
定义local环境后,循环注册该module
下的 mutations
actions
getter
registerMutation
注册 mutation
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
function registerMutation (store, type, handler, local) {
// 通过 namespace 找到当前模块的 mutations
const entry = store._mutations[type] || (store._mutations[type] = [])
// 循环注册mutations
entry.push(function wrappedMutationHandler (payload) {
// 将 store 绑定到每一个 mutation 的 this 上 然后把 local.state 作为第一个参数传入到每一个mutation中,这也是为什么我们在使用commit的时候第一个参数为当前 module 的 state
// commit实际调用的不是我们传入的handler,而是经过封装的
handler.call(store, local.state, payload)
})
}
复制代码
registerAction
注册 action
的过程和 注册 mutation
的过程差不多,需要注意的是 action
被重新封装后 会返回一个 Promise
对象。
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
// 兼容写法
const handler = action.handler || action
registerAction(store, type, handler, local)
})
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload) {
// 传入 dispatch commit 等参数供我们调用 action 时作为第一个参数使用
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload)
// 使用 action 需要 浏览器支持 Promise
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
}
})
}
复制代码
registerGetter
getters
处理同理
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
function registerGetter (store, type, rawGetter, local) {
// getters只允许存在一个处理函数,getter重复需要报错
if (store._wrappedGetters[type]) {
if (__DEV__) {
console.error(`[vuex] duplicate getter key: ${type}`)
}
return
}
// 存储封装后的getters处理函数
store._wrappedGetters[type] = function wrappedGetter (store) {
// 传入 state getters 等参数供我们调用 getters 时作为第一个参数使用
return rawGetter(
local.state, // local state
local.getters, // local getters
store.state, // root state
store.getters // root getters
)
}
}
复制代码
这样我们就给 store 注册了 actions mutations getters
resetStoreVM
这一步主要就是让 store
中的 state
和 getter
实现响应的一步,把 state
和 getter
都绑定到一个 Vue
的实例上实现数据的响应式。
function resetStoreVM (store, state, hot) {
// 缓存之前的 vue 实例
const oldVm = store._vm
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = partial(fn, store)
// 通过Object.defineProperty为每一个getter方法设置get方法,比如获取this.$store.getters.test的时候获取的是store._vm.test,也就是Vue对象的computed属性
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
const silent = Vue.config.silent
Vue.config.silent = true
// 这里 new Vue() 并把 state 和 getter 绑定到实例上 实现响应式
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
// 解除旧的vue实例对state的引用,并销毁旧的Vue对象
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
复制代码
简单来说就是通过 new Vue()
把 state
和 getter
绑定到 Vue
的实例上 实现响应式,然后吧 Vue
的实例绑定到 store
的 _vm
属性上。
到这里 Store
的实例化过程的主流程就分析完毕了。
最后来看看我们最常用到的 commit
和 dispatch
方法
commit
commit (_type, _payload, _options) {
// 处理不同的传参方式
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 找到对应的 mutation 方法
const entry = this._mutations[type]
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
// 通过 _withCommit 来执行 mutation
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 订阅者函数遍历执行,传入当前的mutation对象和当前的state
this._subscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.forEach(sub => sub(mutation, this.state))
if (
__DEV__ &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
复制代码
_withCommit
_withCommit (fn) {
const committing = this._committing
// 只有通过 _withCommit 执行 mutation '_committing' 才为 true
this._committing = true
fn()
// 执行完毕 _committing 重新置为 false
// 这也是为什么 mutation 必须为 同步函数的原因,只有为同步函数才能正确的等待 mutatuion执行完毕后修改 _committing 的值
this._committing = committing
}
// 监听对state的修改,若 _committing 不为 true 则抛出警告
function enableStrictMode (store) {
// this._data.$$state 指向 store 的 state
store._vm.$watch(function () { return this._data.$$state }, () => {
if (__DEV__) {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}
}, { deep: true, sync: true })
}
复制代码
dispatch
dispatch (_type, _payload) {
console.log(this, 'this')
// check object-style dispatch
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
// 找到对应的 action 方法
const entry = this._actions[type]
if (!entry) {
if (__DEV__) {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
try {
this._actionSubscribers
.slice() // shallow copy to prevent iterator invalidation if subscriber synchronously calls unsubscribe
.filter(sub => sub.before)
.forEach(sub => sub.before(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in before action subscribers: `)
console.error(e)
}
}
// 执行 action 并返回一个 Promise
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
try {
this._actionSubscribers
.filter(sub => sub.after)
.forEach(sub => sub.after(action, this.state))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in after action subscribers: `)
console.error(e)
}
}
resolve(res)
}, error => {
try {
this._actionSubscribers
.filter(sub => sub.error)
.forEach(sub => sub.error(action, this.state, error))
} catch (e) {
if (__DEV__) {
console.warn(`[vuex] error in error action subscribers: `)
console.error(e)
}
}
reject(error)
})
})
}
复制代码
最后
文章只介绍了 vuex 的主流程代码,源码中还有一些辅助函数、工具函数类似mapState
、mapGetters
、mapActions
、mapMutations
等等,有兴趣的可以打开源码看看。
第一次写文章,如果大家发现有错误或者疑问,再或者哪里没写明白的地方,欢迎评论指出。