什么是Vuex?
Vuex 是一个专为 Vue.js 应用程序开发的用于管理页面数据状态、提供统一数据操作的生态系统。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
针对以下问题进行源码解析
- vuex中的store实例是如何注入到vue的每个组件上的?
- state内部是如何实现支持模块配置和模块嵌套的?
- 在执行dispatch触发action(commit同理)的时候,只需传入(type, payload),action执行函数中第一个参数store从哪里获取的?
- vuex中的state如何和vue关联起来?
- 如何区分state是外部直接修改,还是通过mutation方法修改的?
- 调试时的“时空穿梭”功能是如何实现的?
vuex的核心流程
- Vue Components: vue中的组件用户操作的时候执行dispatch触发actions;
- dispatch: 操作actions的方法,并且是唯一操作Actions的方法;该方法返回一个promise对象;
- Actions: 处理同步或异步的操作,支持多个同名的方法,按照注册顺序触发执行;api的接口请求逻辑就写在此处;该方法支持返回一个promise对象,以便链式调用;
- commit: 操作mutations的方法,并且是唯一操作mutations的方法;
- mutations: 修改State状态的唯一方法;其他方法修改state在严格模式下会报错;并且方法名是唯一的;
- state: 状态对象;
源码解析
目录结构
- module.js: 封装了Module类,此类的目的就是统一state数据的格式;
- module-collection.js: 递归处理state数据,把state中的子模块全部转成统一的格式;
- plugins: 修改日志等内部的插件;
- helpers.js:提供action、mutations以及getters的查找API;
- index.js: 源码的入口,封装了Store类;
- util.js: 提供了工具方法如find、deepCopy、forEachValue以及assert等方法;
install方法
vuex是通过插件的形式注入到vue中
Vue.use(Vuex)
Vue.use方法中执行了Vuex的install方法,因此Vuex中有install方法;
let Vue
function install (_Vue) {
// 已经被注册过就直接返回
if (Vue && _Vue === Vue) {
{
console.error(
'[vuex] already installed. Vue.use(Vuex) should be called only once.'
);
}
return
}
// 保存vue实例
Vue = _Vue;
// 执行applyMixin
applyMixin(Vue);
}
install放中进行了重复判断,并且保存了Vue的实例,执行了applyMixin方法;
function applyMixin (Vue) {
// 获取到vue的版本号
var version = Number(Vue.version.split('.')[0]);
// 如果版本大于等于2就通过mixin方法混入beforeCreate钩子
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit });
} else { // 小于2的版本通过重写vue的_init方法
var _init = Vue.prototype._init;
Vue.prototype._init = function (options) {
if ( options === void 0 ) options = {};
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit;
_init.call(this, options);
};
}
function vuexInit () {
// 获取到vue的options
var options = this.$options;
// store injection
// 如果options存在store表示当前为父组件
if (options.store) {
this.$store = typeof options.store === 'function'
? options.store()
: options.store;
// 否则就是子组件,通过parent获取到父级上的$sore赋值给子实例的$store
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
}
applyMixin方法先获取到vue的版本号,通过版本号判断vue是否提供了mixin方法,如果大于等于2的版本就使用mixin混入的方式,把beforeCreate混入到各个组件中;如果是小于2的版本,重写vue上的init方法;init和beforeCreate中都执行了vuexInit方法;vuexInit方法主要把store挂载到vue实例上的$store属性上,如果是子组件实例就会从父组件的实例上的$store中获取到store,并且赋值给子组件实例上的$store,这样一层一层的就可以从父组件上获取到$store;
Root组件中有App组件,App组件中有A组件;
Store类
export class Store {
constructor (options = {}) {
const {
plugins = [],
strict = false,
devtools
} = options
// 初始化数据
// 是否正则修改State数据
this._committing = false
// 存储所有的actions中的方法
this._actions = Object.create(null)
this._actionSubscribers = []
// 存储所有的mutaions中的方法
this._mutations = Object.create(null)
// 存储所有的getters中的方法
this._wrappedGetters = Object.create(null)
// 通过ModulesCollection方法修改数据的格式
this._modules = new ModuleCollection(options)
// 存储命名空间的map
this._modulesNamespaceMap = Object.create(null)
// 存储subscribers
this._subscribers = []
const store = this
const { dispatch, commit } = this
// 重写dispatch,把store传递进去
this.dispatch = function boundDispatch (type, payload) {
return dispatch.call(store, type, payload)
}
// 重写commit,把store传递进去
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都放在根模块的State中,
//所有模块下的mutations,actions,getter都存储到根下的
//_mutations,_actions,_wrapperGetters
installModule(this, state, [], this._modules.root)
// 创建vue实例,把vue和store关联起来
resetStoreState(this, state)
// 执行插件
plugins.forEach(plugin => plugin(this))
}
}
Sotre类的构造函数中,主要初始化一些属性,重写了dispatch和commit方法,目的就是把store作为第一个参数传递进去;通过ModuleCollection类修改数据的格式方便后面的操作,通过installModule方法把数据中所有的getter,actions,mutations提取出来全部放在根对象下对应的属性上;通过resetStoreState方法创建vue实例把state数据和vue关联起来;最后执行所有的插件;下面进行每个方法的具体分析,看完下面的分析之后,再回到这里进行整体分析就会把整个过程串联起来了;
ModuleCollection类
ModuleCollection类主要是递归处理用户传递进来的State数据,把数据转成统一的格式;
// 用户传递进来的State数据
const store = new Vuex.Store({
state: {
age: 100
},
getters: {
myAge(state){
return state.age + 20
}
},
mutations: {
add(state, payload) {
state.age += payload
}
},
actions: {
add ({ commit }, payload) {
return new Promise(res => {
setTimeout(() => {
commit('add', payload)
res()
}, 2000)
})
}
},
modules: { // 使用模块的时候一定要有命名空间,否则会出问题
a: {
namespaced: true,
state: {
age: 1
},
getters: {
myAge(state){
return state.age + 20
}
},
mutations: {
add(state, payload) {
state.age += payload
}
},
modules: {
b: {
namespaced: true,
state: {
age: 'bbb'
},
mutations: {
add(state, payload) {
state.age += payload
}
}
}
}
},
c: {
namespaced: true,
state: {
age: 22
},
mutations: {
add(state, payload) {
state.age += payload
}
}
}
}
})
根下面有a和c两个子模块,a下有b子模块;
通过ModuleCollection类转换之后的结果如下:
每个模块的格式都是由context、runtime、state、_children、_rawModule组成;
- context: 存储Store中的两个方法,和当前模块下的state和getters(包括子模块);
- state: 当前模块下的state数据;
- _children: 当前模块下的子模块;
- _rawModule: 当前模块;
export default class ModuleCollection {
constructor (rawRootModule) {
this.register([], rawRootModule, false)
}
// 通过模块名的集合获取到对应的父模块
get (path) {
return path.reduce((module, key) => {
return module.getChild(key)
}, this.root)
}
register (path, rawModule, runtime = true) {
// 通过Module类创建每个模块的实例,此类主要是定义模块的格式
const newModule = new Module(rawModule, runtime)
// 如果path的长度为0,表示此时为根模块
if (path.length === 0) {
this.root = newModule
} else { // 否则就是子模块
// 获取到当前子模块的父级
const parent = this.get(path.slice(0, -1))
// 把当前子模块放在父级的_children中
parent.addChild(path[path.length - 1], newModule)
}
// 如果当前模块有子模块就进行遍历,通过递归的方式给当前模块下的子模块进行处理
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
// 递归处理,把当前模块的名称和之前模块的名称合并起来,这样就可以通过path获取到父级
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
ModuleCollection类中主要通过Module类创建每个模块的实例,给每个模块统一数据格式;通过path的长度判断是根模块还是子模块,如果是子模块就通过遍历path获取到对应的父模块,把当前子模块存储到父模块的_children中,如果当前模块有modules,就进行遍历并且通过递归的形式处理子模块;
Module类
Module类主要是创建带有state,,runtime,_children,_rawModule属性的实例,并且提供操作_children的方法;提供遍历getter,actions,mutations的方法;
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
// 初始_children
this._children = Object.create(null)
// 初始_rawModule,直接保存传递进来的rawModule
this._rawModule = rawModule
const rawState = rawModule.state
// 初始state
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
// 获取命名空间
get namespaced () {
return !!this._rawModule.namespaced
}
// 给当前模块下的_children添加子模块
addChild (key, module) {
this._children[key] = module
}
// 删除当前模块下的_children下指定的子模块
removeChild (key) {
delete this._children[key]
}
// 获取当前模块下的_children下指定的子模块
getChild (key) {
return this._children[key]
}
// 判断当前模块下的_children下是否有指定的子模块
hasChild (key) {
return key in this._children
}
// 遍历当前模块下的_children
forEachChild (fn) {
forEachValue(this._children, fn)
}
// 遍历当前模块下的Gettes
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)
}
}
}
以上就分析完ModuleCollection和Module类,这两个类主要是把用户传递进来的数据转成指定的格式,方便后续的操作;数据格式处理完成之后,就需要通过installModule方法对各个模块下的state,actions,getters,mutaion进行处理;
installModule方法
Store构造函数中通过ModuleCollection处理完数据格式之后,就通过installModule方法对格式化的数据进行了处理;
export function installModule (store, rootState, path, module, hot) {
// 是否是根
const isRoot = !path.length
// 获取到对应的命名空间
const namespace = store._modules.getNamespace(path)
// 如果当前模块中定义了命名空间就把当前模块存储到_modulesNamespaceMap中
if (module.namespaced) {
store._modulesNamespaceMap[namespace] = module
}
// 如果不是根 就是子模块
if (!isRoot && !hot) {
// 通过path模块名路径获取到对应的父级的State属性
const parentState = getNestedState(rootState, path.slice(0, -1))
// 获取到当前的模块名
const moduleName = path[path.length - 1]
// 通过_withCommit包裹执行修改State的方法
store._withCommit(() => {
// 把当前模块的State存储到父级的State下
parentState[moduleName] = module.state
})
}
// 给当前模块上添加Context属性
const local = module.context = makeLocalContext(store, namespace, path)
// 遍历当前模块下的mutation,通过registerMutation函数把每一个mutation中的方法都存储到根对象下的_mutations属性中
module.forEachMutation((mutation, key) => {
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 遍历当前模块下的action,通过registerAction函数把每一个action中的方法都存储到根对象下的_actions属性中
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 遍历当前模块下的getter,通过registerGetter函数把每一个getter中的方法都存储到根对象下的_wrappedGetters属性中
module.forEachGetter((getter, key) => {
const namespacedType = namespace + key
registerGetter(store, namespacedType, getter, local)
})
// 遍历当前模块下的modules,通过递归进行处理子模块
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
以上是把子模块下的State属性都存放到根对象下的State中,并且通过_withCommit方法进行包裹,目的就是在严格模式下只能通过mutations修改State,其他方法都不能修改State;遍历当前模块下的mutation,action,getter,modules,把对应下面的每个方法都存储到根对象下对应的属性中;
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)
})
}
function registerAction (store, type, handler, local) {
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)
// 如果返回的结果不是promise,那么就通过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
}
})
}
function registerGetter (store, type, rawGetter, local) {
// 已经存在就直接返回
if (store._wrappedGetters[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
)
}
}
以上就完成了下面格式的处理
如图可以看到_mutaion和_wrapperGetters下的key都带有/,是因为根据模块进行定义key,a模块下的add就是a/add;a模块下的b模块下的add就是a/b/add,那么这个是怎么做到的?
// 获取到对应的命名空间
const namespace = store._modules.getNamespace(path)
主要通过ModuleCollection类下的getNamespace实现的,回过来看下此方法是怎么实现的
export default class ModuleCollection {
...
getNamespace (path) {
// 获取到根模块
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
}
遍历path,通过path中的模块名,不断的获取到对应的模块并且进行替换,如果有命名空间就通过/进行拼接,path遍历完就返回一个完整的路径;
以上就完成了store数据的格式修改,并且把对应的数据都添加到了根对象下对应的属性中;下面就是把Store中的数据和vue进行关联起来了;
resetStoreState方法
function resetStoreVM (store, state, hot) {
// 存储上个vue实例
const oldVm = store._vm
// 存储getters
store.getters = {}
// 缓存getters
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
// 遍历getters
forEachValue(wrappedGetters, (fn, key) => {
// 存储到computed对象中
computed[key] = partial(fn, store)
// 给store.getters上定义Getter,并且是通过_vm[key]进行代理
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
const silent = Vue.config.silent
Vue.config.silent = true
// 创建vue实例
store._vm = new Vue({
data: {
$$state: state // 把state放在Data下的$$state属性上
},
computed, // 计算属性
})
Vue.config.silent = silent
// enable strict mode for new vm
if (store.strict) {
enableStrictMode(store)
}
// 如果老的存在就进行销毁
if (oldVm) {
if (hot) {
store._withCommit(() => {
oldVm._data.$$state = null
})
}
Vue.nextTick(() => oldVm.$destroy())
}
}
function partial (fn, arg) {
return function () {
return fn(arg)
}
}
resetStoreVM函数中主要创建了vue实例,遍历根上_wrappedGetters属性,把每个Getter作为vue的计算属性,并且把每个getter通过代理的形式又定义在Store下的getters属性上;把state属性作为vue的data下的$$state属性;这样在vue中操作的store下的state都是响应的;
完善Store上的其他方法
访问store上的state属性
get state () {
return this._vm._data.$$state
}
Store.state其实就是访问Store._vm._data.$$state属性;
commit方法
commit (_type, _payload, _options) {
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
// 获取到对应的mutations
const entry = this._mutations[type]
// 如果不存在就返回
if (!entry) {
return
}
// 通过_withCommit方法进行包裹
this._withCommit(() => {
// 遍历执行mutations
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
// 如果存在_subscribers就遍历执行
this._subscribers
.slice()
.forEach(sub => sub(mutation, this.state))
}
commit方法中获取当对应的mutations,遍历这个mutaions进行执行,并且通过_withCommit方法包裹执行的;
dispatch方法
dispatch (_type, _payload) {
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
// 获取到对应的actions
const entry = this._actions[type]
if (!entry) {
return
}
// 如果Action的长度大于1就通过promise.all进行执行,否则直接执行第一个函数
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
// 返回一个promise
return new Promise((resolve, reject) => {
result.then(res => {
resolve(res)
}, error => {
reject(error)
})
})
}
dispatch方法主要获取到对应的actions,如果actions数组的长度大于1就通过Promise.all进行执行,否则就执行数组的第一个函数,最后返回一个promise;
replaceState方法
replaceState (state) {
this._withCommit(() => {
this._vm._data.$$state = state
})
}
通过_withCommit包裹修改了_vm._data.$$state
_withCommit方法
_withCommit (fn) {
this._committing = true
fn()
this._committing = false
}
进入_withCommit方法的时候把committing属性设置为true,表示此时正在执行fn函数,当fn执行完毕之后就会改为false;此方法主要是防止用户调用mutation之外的方法修改State;不是通过mutation方法修改state,那么_commiting属性是false,在开发环境的严格模式下就会报错;
registerModule方法
registerModule (path, rawModule, options = {}) {
if (typeof path === 'string') path = [path]
this._modules.register(path, rawModule)
installModule(this, this.state, path, this._modules.get(path), options.preserveState)
// reset store to update getters...
resetStoreVM(this, this.state)
}
registerModule方法是动态添加模块的,函数内部和Store构造函数内部一样,先通过register进行模块的格式修改,并且根据path会添加到对应的父级的_children中;再通过installModule进行action,state,getters,mutations属性的处理,全部放在根对象的对应属性下;最后通过resetStoreVM把state和vue关联起来;
subscribe方法
subscribe主要是订阅 store 的 mutation;handler 会在每个 mutation 完成后调用,接收 mutation 和经过 mutation 后的状态作为参数:
subscribe (fn, options) {
return genericSubscribe(fn, this._subscribers, options)
}
function genericSubscribe (fn, subs, options) {
if (subs.indexOf(fn) < 0) {
options && options.prepend
? subs.unshift(fn)
: subs.push(fn)
}
return () => {
const i = subs.indexOf(fn)
if (i > -1) {
subs.splice(i, 1)
}
}
}
subscribe函数主要是把传递进去的回调存储到_subscribers属性中,并且返回一个函数,此函数中会从_subscribers中删除此回调;_subscribers属性是在mutations执行的时候遍历执行的;
devtool.js
const target = typeof window !== 'undefined'
? window
: typeof global !== 'undefined'
? global
: {}
const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__
export default function devtoolPlugin (store) {
if (!devtoolHook) return
store._devtoolHook = devtoolHook
// 触发vuex:init方法,传递store
devtoolHook.emit('vuex:init', store)
// 定义时光穿梭机,通过replaceState替换传递进来的state
devtoolHook.on('vuex:travel-to-state', targetState => {
store.replaceState(targetState)
})
// mutation被执行时,触发hook,并提供被触发的mutation函数和当前的state状态
store.subscribe((mutation, state) => {
devtoolHook.emit('vuex:mutation', mutation, state)
}, { prepend: true })
}
mapState的实现
function mapSate(list){
const obj = {}
list.forEach((key, value) => {
obj[value] = function () {
return this.$store.state[value]
}
})
return obj
}
mapSate方法其实很简单,就是把传递进来的数组进行遍历,从state中找到对应的值,保存到一个新的对象中,最后返回这个对象;那么在使用的时候就可以通过扩展运算符进行展开,其他的map方法都是类似的实现;
源码分析到这里,Vuex框架的实现原理基本都已经分析完毕。
总结
- 通过vue.use插件的形式使用vuex,vuex的install方法中通过mixin混入把beforeCreate钩子混入到每个子组件中;beforeCreate钩子中通过父级获取到
$store并且把它赋值给子级的$store,让每个子组件都能获取到$store实例; - vuex的Store类中通过ModuleCollection和Module类把传递进来的数据的格式进行统一处理;
- 把处理完成之后的数据通过installModule方法,把每个模块下的actions,getters,state,mutations都存储到根对象的对应属性中,如果开启了命名空间那么mutations和getters的key是通过模块的名称进行拼接;
- 数据处理完成之后,就通过resetStoreVM方法创建vue实例,把state放在Vue的data下的$$state中,把geter作为计算属性,此时就把vue和store关联起来了;
- commit方法就是执行指定对应的mutations;
- dispatch方法执行对应的Action并且返回一个promise对象;
- registerModule动态添加模块的方法,内部重新执行了1,2,3步;
- replaceState方法直接替换vue实例上data下的$$state属性;
面试题
-
vuex中的store实例是如何注入到vue的每个组件上的?
首先通过mixin把beforecreate钩子混入到每个组件中,在钩子中通过把$options的$store赋值给当前实例的$store上,如果是子组件,就可以通过parent.$store获取到父组件的store,把此store再赋值给自身的$store属性,这样就实现了注入到每个组件上;使用beforecreate钩子是因为在此阶段已经初始好了$options属性,可以通过$options获取到store对象; -
state内部是如何实现支持模块配置和模块嵌套的?
把子模块放在当前模块的_children中,把所有模块的Actions和muataions,geters,state都放在根对象下的对应属性中;操作某个子模块下的方法或属性,就可以直接从根对象下的对应的属性中获取到; -
在执行dispatch触发action(commit同理)的时候,只需传入(type, payload),action执行函数中第一个参数store从哪里获取的?
在Store构造函数中重写了这两个方法,把当前的store作为第一个参数传递进去; -
vuex中的state如何和vue关联起来?
通过创建vue的实例,把state放在vue下的data下的$$state属性中,把getters作为vue的计算属性; -
如何区分state是外部直接修改,还是通过mutation方法修改的?
vuex内部修改State都是通过一个函数进行包裹执行的,此函数在修改之前会把一个变量设置为true,当修改完成之后改为false;如果外部修改的话此变量都是false,watch会监听state的变化,并且此变量为false就会报错; -
调试时的“时空穿梭”功能是如何实现的?
devtoolPlugin中提供了此功能。因为dev模式下所有的state change都会被记录下来,’时空穿梭’ 功能其实就是将当前的state替换为记录中某个时刻的state状态,利用store.replaceState(targetState)方法将执行this._vm.state = state实现
参考: