1 Vuex介绍
Vuex
是尤雨溪为vue
编写的状态管理器。
在我们项目开发过程中,总是会有一部分信息是所有组件都需要访问到的,比如登录用户信息,如果仅通过父子组件之间通信来传递这些信息,那么当组件的数量到达一定规模之后,将会给你的代码带来噩梦级别的灾难。
由于现代框架都是以数据驱动视图,对于数据的管理就非常重要。Vuex
就是为了解决这件事的,统一管理公用的状态,不仅使代码简洁易于维护,开发人员使用起来也更便捷。
阅读源码建议分以下步骤进行:
- 从全局思考:在对项目的的功能和使用有一定了解的情况下,站在全局的角度,明白该项目的目标是要解决什么问题,思考或者猜想作者是如何设计以实现该目标的。
- 从局部出发,以点破面:从最基础的配置使用出发,怎样的配置产生了怎样结果?配置和产生的结果之间有什么关系,从源码里面找到是如何实现这些关系的。
- 边实践边阅读:在阅读过程中,可能会遇到晦涩难懂的代码,比如看不懂函数的作用,那么可以尝试自己设计参数传入,看看返回什么结果,也可以在代码中打断点,查看该代码发挥了什么作用。
- 书读百遍,其意自现:刚开始看代码的时候,往往一头雾水,静下心来,多读几遍,肯定会慢慢地加深理解的。
下面就从一个简单的例子出发解读Vuex的源码。
2 从例子出发
js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
// 定义一个一级模块:user
user: {
namespaced: true,
state: {
name: 'lsm',
age: 18,
},
getters: {},
mutations: {
setName: (state, name) => {
state.name = name;
},
setAge: (state, age) => {
state.age = age;
},
},
actions: {},
modules: {
// 定义二级模块 privateUser
privateUser: {
namespaced: true,
state: {
type: 'private',
},
getters: {},
mutations: {
setType: (state, type) => {
state.type = type;
},
},
actions: {},
},
// 定义二级模块 publicUser
publicUser: {
namespaced: true,
state: {
type: 'public',
},
getters: {},
mutations: {
setType: (state, type) => {
state.type = type;
},
},
actions: {},
},
},
}
},
});
上述的代码非常简单,首先定义了一个一级模块user
,又在user
下面定义了两个二级模块privateUser
和publicUser
,并且namespace
都填写了true
。
让我们将this.$store
打印出来查看一下:
首先注意到我们代码里常用的commit
,dispatch
方法,以及getters
和state
对象。 另外还有许多以下划线开头的内部变量,比如_modules
、_modulesNamespaceMap
等等。
接下来让我们点开state
和_modules
查看
在state
中,模块名称转为了对象的键名,并且拼接成嵌套对象。
在_modules
中,可以看到root
、user
、privateUser
、publicUser
,并且他们也是嵌套结构,另外这四个都是Module
类,可以猜想到两者之间一定有某些联系,并且代码中会有Module
类。
最后再打开_modulesNamespaceMap
查看一下内容,这不就是我们调用模块mutations
或者actions
时写的前缀吗?
this.$store.commit('user/setName','lsm')
,想起来了吗?
3 带着这些疑问开始源码分析
3.1 目录文件简介
arduino
├─src
│ │ helpers.js //语法糖函数,mapState,mapGetters,mapMutations,mapActions
│ │ index.cjs.js
│ │ index.js //三个index都是最终导出的文件
│ │ index.mjs
│ │ mixin.js //安装到vue
│ │ store.js //Store类
│ │ util.js //项目中的通用方法
│ │
│ ├─module
│ │ module-collection.js //模块收集类
│ │ module.js //单个模块类
│ │
│ └─plugins
│ devtool.js //devtool插件
│ logger.js //日志插件
3.2 index.js
js
export default {
Store, //Store类
install, //install方法
version: '__VERSION__',
mapState, //四个语法糖函数
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers,
createLogger
}
从这里就可以看出为什么我们创建使用Vuex是这么写的了:
const store=new Vuex.Store({....})
另外Vue.use(Vuex)
,执行的install
方法也找到了。
3.3 inatall
跳转代码,可以知道install
实际执行的是mixin.js
里的方法,作用是将store
挂载到所有的vue
组件上
js
export default function (Vue) {
// 获取vue主版本号
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
// 如果版本号为2及以上,在vue组件的beforeCreate钩子中混入vuexInit方法,
// 也就是所有组件创建之前都会执行一次这个方法
Vue.mixin({ beforeCreate: vuexInit })
} else {
// 如果版本为1,则挂载到vue原型上
const _init = Vue.prototype._init
Vue.prototype._init = function (options = {}) {
options.init = options.init
? [vuexInit].concat(options.init)
: vuexInit
_init.call(this, options)
}
}
/**
* Vuex init hook, injected into each instances init hooks list.
*/
// 组件创建时,从$options或者父组件中获取store,设置给当前组件的this.$store
// 这就是为什么所有组件都能够直接通过this.$store访问到vuex
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
}
}
}
3.4 Store类
js
export class Store {
constructor (options = {}) {
// 初始化类实例的内部变量
this._committing = false
this._actions = Object.create(null)
this._actionSubscribers = []
this._mutations = Object.create(null)
this._wrappedGetters = Object.create(null)
// 此处需要特别注意,我们传入的options实际上是用来创建一个ModuleCollection实例,也就是_modules
this._modules = new ModuleCollection(options)
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
// 此处创建了一个Vue对象,
this._watcherVM = new Vue()
this._makeLocalGettersCache = Object.create(null)
// 给commit和dispatch绑定this为实例本身,
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)
}
// strict mode
this.strict = strict
// 获取_modules根模块的state
const state = this._modules.root.state
// 初始化根模块,并且递归初始化所有子模块
installModule(this, state, [], this._modules.root)
// 初始化数据响应式系统,使所有state都可以动态响应
resetStoreVM(this, state)
}
//
get state () {
return this._vm._data.$$state
}
// 根据type从_mutations找到mutation方法,执行时传入payload
// 看后续代码需要注意_mutations是怎么来的
commit (_type, _payload, _options) {
// 将对象格式的入参转为字符串形式
const {
type,
payload,
options
} = unifyObjectStyle(_type, _payload, _options)
const mutation = { type, payload }
const entry = this._mutations[type]
this._withCommit(() => {
entry.forEach(function commitIterator (handler) {
handler(payload)
})
})
}
// 根据type从_actions找到action方法,执行时传入payload,最后处理成Promise返回
// 看后续代码需要注意_actions是怎么来的
dispatch (_type, _payload) {
// 将对象格式的入参转为字符串形式
const {
type,
payload
} = unifyObjectStyle(_type, _payload)
const action = { type, payload }
const entry = this._actions[type]
const result = entry.length > 1
? Promise.all(entry.map(handler => handler(payload)))
: entry[0](payload)
return new Promise((resolve, reject) => {
result.then(res => {
resolve(res)
}, error => {
reject(error)
})
})
}
// 这是commit的包裹函数,在执行commit之前会把_committing改为true,
// 之后再修改回来
// 在开启严格模式下,如果发现state变化但是_committing不为true,
// 就说明不是通过commit修改state,将会告警报错
_withCommit (fn) {
const committing = this._committing
this._committing = true
fn()
this._committing = committing
}
}
看到这里,我们发现有三个地方一定是非常重要的,分别是
this._modules = new ModuleCollection(options)
installModule(this, state, [], this._modules.root)
resetStoreVM(this, state)
我们把这三个地方搞清楚了,基本上就理解了vuex的工作原理与设计。
3.5 ModuleCollection类
说明:
- path格式是这样的:
[]
代表根模块
['user']
代表获取user模块
['user','privateUser']
代表获取privateUser模块。
forEachValue
用来遍历对象的键值对,回调函数的参数分别代表对象的值和对象的键
ModuleCollection
构造函数仅执行一个注册函数register
,那么注册函数就是关键,需要重点看。
js
export default class ModuleCollection {
constructor (rawRootModule) {
// 注册根模块
this.register([], rawRootModule, false)
}
// 根据路径获取模块
get (path) {}
// 用/分割,组成namespace
getNamespace (path) {}
// 更新模块
update (rawRootModule) {}
// 注册模块
register (path, rawModule, runtime = true) {
// 创建Module实例,rawModule其实就是我们创建Store传入的options
const newModule = new Module(rawModule, runtime)
if (path.length === 0) {
// 根模块
this.root = newModule
} else {
// 这段代码要在下面的递归执行后才会进入到,将子节点挂到父节点的_children下
// slice(0, -1)表示从0开始到倒数第二个
const parent = this.get(path.slice(0, -1))
parent.addChild(path[path.length - 1], newModule)
}
// register nested modules
// 如果配置信息里有包含modules,则进行递归注册,注册完后会挂载到父模块的_children下
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
// 取消注册
unregister (path) {}
// 判断是否注册
isRegistered (path) {}
}
3.6 Module类
Module的构造函数其实也就是简单的记录信息。
js
export default class Module {
constructor (rawModule, runtime) {
this.runtime = runtime
this._children = Object.create(null)
this._rawModule = rawModule
const rawState = rawModule.state
// 说明state配置可以是函数也可以是对象
this.state = (typeof rawState === 'function' ? rawState() : rawState) || {}
}
get namespaced () {}
addChild (key, module) {
this._children[key] = module
}
removeChild (key) {}
getChild (key) {}
hasChild (key) {}
update (rawModule) {}
forEachChild (fn) {}
forEachGetter (fn) {}
forEachAction (fn) {}
forEachMutation (fn) {}
}
所以this._modules = new ModuleCollection(options)
这段代码的作用就是创建一个模块树实例,再回头看这张图片可能就更好理解了。
3.7 installModule
接下来看看注册组件方法写了什么内容,传入的参数分别是store
的实例、根模块state
、代表根模块的路径、根模块实例。
installModule(this, state, [], this._modules.root)
js
export function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
// 根据路径获取对应模块的namespace路径,从上到下,用/拼接在一起
const namespace = store._modules.getNamespace(path)
// 如果模块开启了namespace
if (module.namespaced) {
// 则将路径注册到_modulesNamespaceMap对象中,对应的值是整个模块实例
store._modulesNamespaceMap[namespace] = module
}
// set state
if (!isRoot && !hot) {
// 递归的时候才会执行到这里,将子模块的state挂到父模块的state下,键为模块名称
// 从这里可以看出,如果父模块state中定义了和子模块名称一样的变量,会被子模块覆盖掉
// {
// state:{
// user:'lsm'
// },
// modules:{
// user:{state:{}} //这里的user模块会覆盖上面的user,测试环境会有告警
// }
// }
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
// 生成各个模块对应的上下文,里面包含commit和dispatch方法
const local = module.context = makeLocalContext(store, namespace, path)
// 遍历模块的mutations,将其注册到store实例的_mutations中,
// 可以注册多个同名的mutations
module.forEachMutation((mutation, key) => {
// 这里的namespacedType形如 'user/setName',所以我们调用commit的
// 时候才要这么写 this.$store.commit('user/setName','new name')
const namespacedType = namespace + key
registerMutation(store, namespacedType, mutation, local)
})
// 遍历模块的actions,将其注册到store实例的_actions,
module.forEachAction((action, key) => {
const type = action.root ? key : namespace + key
const handler = action.handler || action
registerAction(store, type, handler, local)
})
// 遍历模块的actions,将其注册到store实例的_wrappedGetters,
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)
})
}
js
// 构建组件内部上下文,主要是提供了dispatch和commit方法,getters和state
// 如果模块没有开启namespace,则使用根模块上的dispatch和commit方法
function makeLocalContext (store, namespace, path) {
const noNamespace = namespace === ''
const local = {
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
}
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
if (!options || !options.root) {
type = namespace + type
}
store.commit(type, payload, options)
}
}
// 设置上下文的getters和state,之所以用函数来写,是因为他们会随着state变化
// 这样每次获取到都是最新的
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
js
// 注册Mutation方法,可以看到同名的mutations方法是可能会有多个,
function registerMutation (store, type, handler, local) {
// 获取已存在的mutations,在此基础上增加
const entry = store._mutations[type] || (store._mutations[type] = [])
entry.push(function wrappedMutationHandler (payload) {
// 给mutations的this绑定store,并且带上state参数
handler.call(store, local.state, payload)
})
}
// 注册actions与mutations类似,不同的是第一个参数加了不少内容,
// 写actions时可以直接获取,并且返回值被处理成promise
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)
if (!isPromise(res)) {
res = Promise.resolve(res)
}
return res
})
}
// 注册getters,并且补充了执行时的参数
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
)
}
}
installModule
方法中主要做了以下事项:
- 将所有模块的
state
拼接到根模块的的state
上 - 构建了模块的上下文
context
,包含commit,dispatch,getters,state
- 将所有模块的
namespace
汇总到实例的_modulesNamespaceMap
对象 - 将所有模块的
mutation
汇总到实例的_mutations
对象 - 将所有模块的
action
汇总到实例的_actions
对象 - 将所有模块的
getters
汇总到实例的_mutations
对象
上述操作完成之后,我们就可以使用this.$store.[commit,dispatch]
这些方法了。
接下来就是利用Vue实现state和getters的响应式更新就行了。
3.8 resetStoreVM
resetStoreVM(this, state)
传入参数:当前实例,也就是store,当前实例的state,前面已经构建好了
该函数执行之后,state和getters就都具备响应性了。
js
function resetStoreVM (store, state, hot) {
const oldVm = store._vm
store.getters = {}
store._makeLocalGettersCache = Object.create(null)
const wrappedGetters = store._wrappedGetters
const computed = {}
forEachValue(wrappedGetters, (fn, key) => {
// 将getters构造成计算属性,并且挂到store的getters对象上
computed[key] = partial(fn, store)
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true // for local getters
})
})
// 将state挂到vue的data中,这样它就拥有了响应性,如果state变化了
// 所有使用到它的地方都会自动更新
store._vm = new Vue({
data: {
$$state: state
},
computed
})
// 开启严格模式,state只能通过mutation来修改
if (store.strict) {
enableStrictMode(store)
}
}
3.9 语法糖函数
语法糖函数设计十分巧妙,下面以mapState为例,讲一讲是如何实现的,其他几个实现都是类似的。
首先回顾一下mapState的基本使用方法:
...mapState(['name','locale'])
或者
...mapState('user',['name','age'])
js
// 处理参数,将namespace转为带斜杆的形式,
// 这样就可以在_moduleNamespaceMap中拿到对应的module实例
// 另外还有一个作用,如果只传入一个参数,namespace设为'',也就是取根模块
function normalizeNamespace (fn) {
return (namespace, map) => {
if (typeof namespace !== 'string') {
map = namespace
namespace = ''
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/'
}
return fn(namespace, map)
}
}
/**
* 也是参数处理,
* 数组转换为对象数组([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ]
* 对象也转为对象数组({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
* @param {Array|Object} map
* @return {Array}
*/
function normalizeMap (map) {
if (!isValidMap(map)) {
return []
}
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
// 因为这是放在vue的computed中使用,这里的this就是代表vue组件
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
// 从_modulesNamespaceMap获取module
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
// 如果有module,则使用module中的state和getters
state = module.context.state
getters = module.context.getters
}
// val也支持函数的形式
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
})
// 最终返回的是一个对象,对象的每个键对应的都是一个函数,
// 这就是为什么可以使用...来解构mapState
return res
})
看完源码可以发现,mapState还可以这么写:
...mapState('user/',{aliasName:'name'})
给state起别名
也可以传入一个函数:
js
...mapState('user',{
desc:(state,getters)=>{
return `姓名${state.name},年龄:${state.age}`
}
)
4 结语
本次源码阅读就介绍到这里,如有错误,还请大家指正!