Vuex 的构建原理

解析源码前,我们先来简单的了解下Vuex。
Vuex 是为 Vue 量身定做的状态管理状态管理模式,目的是解决多个组件共享状态带来的复杂数据流难以为维护的问题。简单的说,就是在多个组件间更工程化、简洁明了的共享数据处理。

一、项目目录

首先,让我先浏览下项目目录。如下:
|— src 源代码
|— module 模块相关
|—module-collection.js
|—module.js
|— plugins
|— devtool.js
|— logger.js
|— helpers.js 辅助函数
|— index.esm.js 入口文件,使用ES Module
|— index.js 入口文件
|— mixin.js 混淆
|— store.js 核心模块
|— util.js 工具函数
二、入口
接着,我们从入口文件 index.js 开始,逐级的去了解 Vuex 的构建。
import { Store, install } from './store'
import { mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } from './helpers'
export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
在入口文件中,我们看到 vuex 这里导出了一个 object,之中包含了核心模块Store,装载模块方法install,版本version,辅助函数mapState、mapMutations、mapGetters、mapActions,命名空间辅助函数createNamespacedHelpers。
三、注入安装
我们的都知道,Vue.js提供了Vue.use方法用来给Vue.js安装插件,通过调用插件内部的 install 方法来安装的(如果插件是对象)。
我们来看这里 install 的实现。
function install (_Vue) {
if (Vue && _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
applyMixin(Vue) // 调用 applyMixin 将 Vuex 混入 Vue
}
让我们再来看下 applyMixin,其实在mixin.js 中只实现了一个混淆,就是将 vuex 混淆注入 vue。
export default function (Vue) {
const version = Number(Vue.version.split('.')[0]) // 获取 vue 的版本号首位
if (version >= 2) {
// 如果 vue 版本2.0以上,调用vue.mixin,在 beforeCreate 的时候执行 vuexInit 注入 vuex
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 如果 vue 1.x 的版本,将 vuexInit 放入 vue 的 _init 方法中
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
function vuexInit () {
const options = this.$options
// 获取实例上的 store
if (options.store) {
// 如果 store 是个函数,执行它获得 store
this.$store = typeof options.store === 'function'
? options.store()
: options.store
} else if (options.parent && options.parent.$store) {
// 当前组件上没有 store,就从父组件上获取 store
this.$store = options.parent.$store
}
}
}
这里对于 vue 做了区分,在 vue 2 以上的版本调用 Vue.mixin 直接混入到 beforeCreate 钩子中,1.0 放入 _init 方法中。
在 vuexInit 方法中,获取当前组件 store,如果不存在再从父组件获取,保证了 store 的一致性,同时把 _this.$store 指向跟 store 实例。
四、Store
(1)构造函数
constructor (options = {}) {
// 判断注入vuex
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
// 非生产环境输出日志
if (process.env.NODE_ENV !== 'production') {
// 判断 Vuex 是否已混入 Vue
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
// 判断是否支持 Promise
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
// 判断是否需要使用 new 关键字来创建 Store 实例
assert(this instanceof Store, `store must be called with the new operator.`)
}
const {
plugins = [], // Vuex 的插件方法,接收 Store 作为唯一参数
strict = false // 严格模式,在严格模式下,通过提交 mutation 之外的方法改变 state 都会报错
} = options
this._committing = false // 是否正在commit
this._actions = Object.create(null) // actions
this._actionSubscribers = [] // actions 订阅者
this._mutations = Object.create(null) // mutations
this._wrappedGetters = Object.create(null) // getters
this._modules = new ModuleCollection(options) // modules收集器
this._modulesNamespaceMap = Object.create(null) // 根据命名空间收集modules
this._subscribers = [] // 订阅者
this._watcherVM = new Vue() // 观察者 Vue 实例
// 把 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)
}
// 严格模式(严格模式下,state 只能通过提交 mutations 来更改,否则会抛出错误)
this.strict = strict
const state = this._modules.root.state
// init root module.
// this also recursively registers all sub-modules
// and collects all module getters inside this._wrappedGetters
// 初始化根模块,递归注册依赖的模块,收集所有模块getters放进_wrappedGetters
installModule(this, state, [], this._modules.root)
// initialize the store vm, which is responsible for the reactivity
// (also registers _wrappedGetters as computed properties)
// 初始化store vm,注册 getters 和计算属性。
resetStoreVM(this, state)
// 调用插件
plugins.forEach(plugin => plugin(this))
// devtool插件
const useDevtools = options.devtools !== undefined ? options.devtools : Vue.config.devtools
if (useDevtools) {
devtoolPlugin(this)
}
}
可以看出来,构造器函数主要做了这几件事情:
1、声明基础变量
2、创建 modules 收集器 、调用 installModule 初始化并且递归注册子模块
3、调用 resetStoreVM 来使 Store 具有“响应式”
(2)模块的加工
首先,我们先来看看 ModuleCollection 是怎样收集保存为 _modules 的。
export default class ModuleCollection {
constructor (rawRootModule) {
// 注册模块
this.register([], rawRootModule, false)
}
// 根据 path 来获取模块
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
// 根据 path 来获取命名空间
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
// 更新 modules
update (rawRootModule) {
update([], this.root, rawRootModule)
}
// 注册方法
register (path, rawModule, runtime = true) {
if (process.env.NODE_ENV !== 'production') {
// 判断模块是否符合规范
assertRawModule(path, rawModule)
}
// 实例化
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// 创建根模块
this.root = newModule
} else {
// 获取父模块
const parent = this.get(path.slice(0, -1))
// 把子模块挂载到父模块上
parent.addChild(path[path.length - 1], newModule)
}
// 递归的注册关联模块
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
// 注销子模块
unregister (path) {
const parent = this.get(path.slice(0, -1))
const key = path[path.length - 1]
if (!parent.getChild(key).runtime) return
parent.removeChild(key)
}
}
可以看到,ModuleCollection 主要是将传入的 options 加工为可用的 _modules 模块,提供了一些方法。
主要做了以下四件事:
1、判断模块是否符合规范
2、封装模块,并递归的封装子模块挂载到父模块上,把所有模块种成一颗_modules树
3、提供一些模块可用方法
接着,我们再看看一个 module 是怎样实例化的吧。
export default class Module {
constructor (rawModule, runtime) {
// 是否正在处理
this.runtime = runtime
// 创建 _children 容器,保存子模块
this._children = Object.create(null)
// 保存未处理的模块
this._rawModule = rawModule
// 保存此模块state
const rawState = rawModule.state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// 是否有命名空间
get namespaced () {
return !!this._rawModule.namespaced
}
// 添加子模块
addChild (key, module) {
this._children[key] = module
}
// 移除子模块
removeChild (key) {
delete this._children[key]
}
// 获取子模块
getChild (key) {
return this._children[key]
}
// 更新保存的_rawModule
update (rawModule) {
this._rawModule.namespaced = rawModule.namespaced
if (rawModule.actions) {
this._rawModule.actions = rawModule.actions
}
if (rawModule.mutations) {
this._rawModule.mutations = rawModule.mutations
}
if (rawModule.getters) {
this._rawModule.getters = rawModule.getters
}
}
// 遍历子模块
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、定义 _rawModule 、state 等基础属性
2、定义 namespaced 属性来获取模块的 namespaced,判断是否有命名空间
3、提供模块的一些基础方法,添加、删除、获取和更新
4、提供遍历的工具方法
(3)模块的注册
平时我们使用 mutations,actions 的时候,只需向 commit/dispatch 传入一个字符串和载荷,就可以更改 state。
那么,vuex 是如何做到的呢。这就轮到 installModule 出马了。
// installModule 是一个私有方法,并不在原型上。
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length // 判断根模块,path 不存在即根模块
const namespace = store._modules.getNamespace(path) // 获取模块的命名空间
if (module.namespaced) {
// 定义模块的命名空间映射Map
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
// 将子模块的 state 挂载到父模块上
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
// 获取执行上下文
const local = module.context = makeLocalContext(store, namespace, path)
// 注册 mutaions,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)
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
installModule的意义是初始化根模块然后递归的初始化所有模块,并且收集模块树的所有getters、actions、mutations、以及state。
主要做了这些事:
1、根据命名空间收集 getters、actions、mutations
2、获取执行上下文来注册 getters、actions、mutations(下面详说)
3、递归的注册子模块的 getters、actions、mutations
(4)dispatch 和 commit 的实现
我们都知道,dispatch 和 commit 有两种提交风格:
// 以 commit 为例
// 标准风格
this.$store.commit('myMutation',{name : 'Shein'});
// 对象风格
this.$store.commit({type: 'myMutation',name : 'Shein'});
所以第一步要统一我们的参数
function unifyObjectStyle (type, payload, options) {
// 判断我们传入的第一个参数是否为对象,
if (isObject(type) && type.type) {
options = payload // options 设为第二个参数
payload = type // 载荷设置为第一个参数
type = type.type // mutation 为 type 值
}
if (process.env.NODE_ENV !== 'production') {
// mutation 方法名不为 string 时,抛出错误
assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
}
return { type, payload, options }
}
现在,我们来看看 commit 函数
commit (_type, _payload, _options) {
// 校正参数并定义
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type] // 获取 mutation 入口
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown mutation type: ${type}`)
}
return
}
this._withCommit(() => {
// 遍历入口,执行 mutation
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
this._subscribers.forEach(sub => sub(mutation, this.state)) // 订阅者
if (
process.env.NODE_ENV !== 'production' &&
options && options.silent
) {
console.warn(
`[vuex] mutation type: ${type}. Silent option has been removed. ` +
'Use the filter functionality in the vue-devtools'
)
}
}
commit 首先矫正了参数,然后再去通过 this._mutations[type] 获取命名空间中所有的 mutations 进行遍历,执行 handle。
那么 dispatch 呢?
dispatch (_type, _payload) {
// 同样是先校正参数
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type] // 获得入口
if (!entry) {
if (process.env.NODE_ENV !== 'production') {
console.error(`[vuex] unknown action type: ${type}`)
}
return
}
try {
// 订阅者
this._actionSubscribers
.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)
}
}
const result = entry.length > 1 // 入口大于1,使用Promist.all
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
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
})
}
与 commit 大同小异,不同的是,在多个同名 actions 存在时候,内部是调用 Promise.all 并发执行的。
看完了 commit 和 dispatch 是怎么触发 mutation 和 action 之后,不禁要问,mutation 和 action 是如何执行的,内部又是如何调用的呢?
以 mutation 为例,再让我们回头看看 mutations 是如何注册的。
module.forEachMutation((mutation, key) => {
// 遍历并注册模块内部的 mutaions
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
我们把 registerMutation 方法抓出来看一下
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
首先通过 type,获取到了所有同名方法的入口(没有的话会新建一个入口),再将执行方法 handler push 到入口中,这里使用了 call 方法,把 this 指向到 store. mutation 接收两个参数,分别是 state 和 payload。
我们再来看下 action 的注册。
function registerAction (store, type, handler, local) {
const entry = store._actions[type] || (store._actions[type] = [])
entry.push(function wrappedActionHandler (payload, cb) {
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
if (!isPromise(res)) {
res = Promise.resolve(res) // 非 promise 转为 promise
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
和 commit 不同,action 的第一个参数会传入 context ,所以在 registerAction 中会通过 local 把dispatch 等方法传进来。其次,如果 actions 不是 Promise,会强制转为 promise,是为了多个同名 action 并发的时候,使用 Promise.all() 来处理。
那么问题来了,local 有什么用?是怎么定义的呢?
把视线拉回 local 定义的地方。
const local = module.context = makeLocalContext(store, namespace, path)
…………
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
// 不启用命名空间,走向 store 的dispatch,启用命名空间,走向模块内部 dispatch。
dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
const args = unifyObjectStyle(_type, _payload, _options)
const { payload, options } = args
let { type } = args
// 判断 options.root 不存在,为分发的 action 加上命名空间
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
}
}
return store.dispatch(type, payload)
},
// commit 和 dispatch 同理
commit: noNamespace ? store.commit : (_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._mutations[type]) {
console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
return
}
}
store.commit(type, payload, options)
}
}
// getters and state object must be gotten lazily
// because they will be changed by vm update
Object.defineProperties(local, {
// 定义 getters,存在命名空间则通过 makeLocalGetters 代理到模块内部的 getters
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
// 获取到模块内部 state
get: () => getNestedState(store.state, path)
}
})
return local
}
local 里会判断是否有命名空间,有的话,返回模块内部的 dispatch、commit 和 getters,state 固定返回模块的 state。
我们再来看看 makeLocalGetters 怎么获取局部 getters。
function makeLocalGetters (store, namespace) {
// 定义代理对象
const gettersProxy = {}
// 定义切割点
const splitPos = namespace.length
Object.keys(store.getters).forEach(type => {
// skip if the target getter is not match this namespace
// 跳过不匹配的getter
if (type.slice(0, splitPos) !== namespace) return
// 获取局部 getter
const localType = type.slice(splitPos)
// 定义代理对象 localType
Object.defineProperty(gettersProxy, localType, {
get: () => store.getters[type],
enumerable: true
})
})
// 返回代理对象
return gettersProxy
}
创建局部的getters就是一个代理的过程,在使用模块内使用(没有加上命名空间的)getters的名字,会被代理到,store实例上那个真正的(全名的)getters。
最后,来看一下 getNestedState
function getNestedState (state, path) {
return path.length
? path.reduce((state, key) => state[key], state)
: state
}
比较简单了,通过 path 获取当前模块的 state。
local 到此已经定义完毕,那么它是用来做什么的呢。
- 注册 mutations
function registerMutation (store, type, handler, local) {
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
handler.call(store, local.state, payload)
})
}
在注册 mutation 的时候,我们传入的是模块内部的state。
2.注册 actions
function registerAction (store, type, handler, local) {
……
let res = handler.call(store, {
dispatch: local.dispatch,
commit: local.commit,
getters: local.getters,
state: local.state,
rootGetters: store.getters,
rootState: store.state
}, payload, cb)
……
}
action 中传入的 context,都会被代理到模块本身的 action 上。
- 注册getters
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
)
}
}
在 getters 中,我们可以任意的组装数据,既可以获取局部的 state 、getters ,也可以获取全局的。
到此,模块相关的已经告一段落。接下来我们来看下 Vuex 怎么依赖Vue核心实现数据的“响应式化”。
(5)响应式更新数据 resetStoreVM
function resetStoreVM (store, state, hot) {
// 保存旧实例
const oldVm = store._vm
// bind store public getters
store.getters = {}
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 enviroment.
// 遍历 wrappedGetters ,为每个 getters 设置 get,映射到 store.vm[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
/* Vue.config.silent暂时设置为true的目的是在new一个Vue实例的过程中不会报出一切警告 */
const silent = Vue.config.silent
Vue.config.silent = true
/* 这里new了一个Vue对象,运用Vue内部的响应式实现注册state以及computed*/
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
/* 使能严格模式,保证修改store只能通过mutation */
if (store.strict) {
enableStrictMode(store)
}
if (oldVm) {
/* 解除旧vm的state的引用,以及销毁旧的Vue对象 */
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())
}
}
resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法,这样我们就可以在组件里访问this.$store.getter.test就等同于访问store._vm.test。
之后Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。
这两步执行完以后,我们就可以通过this.$store.getter.test访问vm中的test属性了。
五、总结
Vuex 主要原理构建的解析就到此结束啦。
总的来说,Vuex 的代码不是很多。但麻雀虽小,五脏俱全,里面的设计模式还是十分优秀,值得我们学习的。我总结几点,在项目中可能会遇到的情况吧(其实文档里大都有写- -):
1、模块内提交 mutation 和 action,需要更改root状态,或者提交root的commit,传入options: {root: true}。
2、没有声明严格模式的情况下,state 可以被直接修改的,需要注意⚠️,声明了严格模式后,任何非提交 mutation 的对 state 都会抛出错误,这能保证所有的状态变更都能被调试工具跟踪到。
3、分发 action 依然会存在“竞态”的问题,需要注意业务逻辑的先后,之所以要分发 action ,是为了每个 mutation 提交后会立即得到一个新的状态。
4、如果不设置namespaced: true,不同模块相同的 mutation 可以同时触发,相同模块同名 mutation 后面的会覆盖前面的。actions 同理。
还有,最好不要用 360 查杀 Vuex 的项目。会报毒!!!!


ENDING....