时隔一年了,又重新把vue源码拿起来了,准备记录下来,也算是对自己知识的一个巩固。
Vuex初始化
vuex作为vue的一个生态插件,也是支持npm发布的,当我们import的时候呢,执行的是vuex/dist/vuex.esm.js这里面的代码


export default {
Store,
install,
version: '__VERSION__',
mapState,
mapMutations,
mapGetters,
mapActions,
createNamespacedHelpers
}
可以知道当我们import vuex的时候,实际上就会返回这样的一个对象,所以我们vuex上也有store这个对象,当我们使用Vue.use(Vue)的时候,就会执行这个install。我们来看下这个install的源码。在src/store.js中
export function install (_Vue) {
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方法,在src/mixin.js中
export default function (Vue) {
const version = Number(Vue.version.split('.')[0])
if (version >= 2) {
Vue.mixin({ beforeCreate: vuexInit })
} else {
// override init and inject vuex init procedure
// for 1.x backwards compatibility.
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.
*/
function vuexInit () {
const options = this.$options
// store injection
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
}
}
}
首先先会对我们的vue版本进行一个判断,1.x和2.x走的都是这一套代码,只是执行的方法不一样,2.x呢我看能看到是执行的Vue.mixin方法,混入beforeCreate这个钩子函数,然后混入了vuexInit,在vuexInit的时候呢,其实就是把我们的store注入进来,我们会看到先去取这个this.$options,当options.store存在的时候呢,判断store是否是函数,如果是函数就去执行,如果不是就直接赋值,如果没有的话,就去找它的parent.$store,通过这种方式,能让我们每一个vue实例都有一个$store对象。这样我们可以在任意的组件中通过this.$store可以访问到我们的store实例。那么我们这个store其实是在new Vue的时候传入的。
const app = new Vue({
el:'#app',
store
})
new Vue.Store实现
对于Store的定义呢是在src/store.js中,我们来分析一下store的这个构造函数的执行逻辑
let Vue // bind on install
export class Store {
constructor(options ={}){
if (!Vue && typeof window !== 'undefined' && window.Vue) {
install(window.Vue)
}
/*
当我们不通过npm的方法去开发,面是通过外链的方式去加载vue,vuex,会在window上注册Vue这个变量,然后需要手动去执行install方法。
*/
if (process.env.NODE_ENV !== 'production') {
assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
assert(this instanceof Store, `Store must be called with the new operator.`)
}
/*
这几行呢就是如果是在非生产环境下有这么几个断言,首先会断言Vue,这个Vue呢就是最头上我们定义的这个Vue,这个Vue其实在执行install方法的时候就会赋值。断言的意义呢就是执行store实例化前呢,我们一定要通过Vue.vue(Vuex)去注册,注册完了才能实例化。
下面也会对Promise进行断言,因为我们Vuex整个库是依赖promise,如果我们的浏览器没有原生支持promise,需要打一个promise的一个补丁。
最后一个是判断this是我们Store的一个实例,我们去执行Store的构造函数的时候,必须是通过new的方式,如果直接调用store的函数就会报出警告。
*/
const {
plugins = [],
strict = false
} = options
/*
定义了一些options的常量,plugins是vuex支持的一些个插件
*/
// store internal state
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) //初始化modules的逻辑
this._modulesNamespaceMap = Object.create(null)
this._subscribers = []
this._watcherVM = new Vue()
/*这些就是在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)
}
/*
定义了一个store,缓存了this,然后通过这个this拿到了dispatch,commit方法,然后重新给它赋值,当我们在执行dispatch的时候,它的上下文就是这个store。
*/
}
}
初始化过程中呢,重点有三个部分,第一个就是new ModuleCollection去初始化这个modules.第二个就是installModule,初始化我们这些个actions呀,wrappedGetters,mutations呀,还有就是去执行resetStoreVM。 下面我们来重点分析下new ModuleCollection.
new ModuleCollection 分析
在src/module/module-collection.js中
export default class ModuleCollection {
constructor (rawRootModule) {
// register root module (Vuex.Store options)
this.register([], rawRootModule, false)
}
/*我们在执行new Module的时候呢就去执行这个constructor,然后将rawRootModule作为参数传入,这个就是我们外面定义的module,然后去执行register方法*/
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)
}
// register nested modules
if (rawModule.modules) {
forEachValue(rawModule.modules, (rawChildModule, key) => {
this.register(path.concat(key), rawChildModule, runtime)
})
}
}
}
这里面呢,我们new Module的方式将我们的module定义传入,这个new Module,在src/module/module.js中,定义了一个Module的类,稍后我会讲这个,也就是说这个module转成了一个实例。 当我们的path长度为0的时候,就将newModule作为根module,然后判断我们是否有这个rawModule.modules,如果有的话就去遍历这个,拿到每一个对应的module,上个图瞅一眼


下面我们分析下installModule的实现
installModule 的实现
installModule(this, state, [], this._modules.root)
先看下要传的值,把store的实例传入,然后是state,然后是path为空数组,
function installModule (store, rootState, path, module, hot) {
const isRoot = !path.length
const namespace = store._modules.getNamespace(path)
/*
根据这个path.length来判断isRoot是否为true,namesapce呢就是module-collection.js中的方法
*/
// set state
if (!isRoot && !hot) {
const parentState = getNestedState(rootState, path.slice(0, -1))
const moduleName = path[path.length - 1]
store._withCommit(() => {
Vue.set(parentState, moduleName, module.state)
})
}
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) => {
installModule(store, rootState, path.concat(key), child, hot)
})
}
getNamespace (path) {
let module = this.root
return path.reduce((namespace, key) => {
module = module.getChild(key)
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
通过path.reduce构造这个namespace,然后namespace又是通过module.getChild去一层层找它的子module。在找的过程中,module.namespaced为true的情况下,对这个值进行一个拼接。然后拿到对应的namespace去做对应的赋值。

下面给我们定义了一个local,关键点在于makeLocalContext函数,我们来看下它主要做了些什么。
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
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: 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: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
}
这个函数最终返回的是一个local对象,在local里面重新定义了dispatch,commit,在函数开始将namespace判断是否为空赋值给了noNamespace,当noNamespace为空的时候,实际上就是我们的store.dispatch,下面有一个重要的点就是将我们的type拼接给了namespace重新赋值给type,拼接完后才会调用store.dispatch。这就是为什么我们在使用的时候,会看到它是拼接的效果。commit也是一样,先是做一些参数的处理,然后再去拼接namespace,getters也是这样的思路。这个就是我们localContext的实现。返回来的local呢会在下面四个forEach...函数会去引用。
下面来分析下这四个函数。 1.首先来看下mutation的一个注册过程,它实际上会去执行module.js中的
forEachMutation (fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
看我们定义的module下面有没有mutations,如果有的话就会去遍历。遍历完后会去执行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)
})
}
实际上创建的是一个_mutations对应的数组,_mutations[type]如果不存在,就是一个空数组。然后将wrappedMutationHandler push到这个数组中,然后这个wrappedMutationHandler执行的时候会去执行handler,可以看到handler.call的时候store是这个上下文,然后是local.state,所以在

2.然后就是来看registerAction。 它其实和mutation是类似的。我们看到action有一个配置是action.root,如果存在不用去拼接namespace,否则还是需要去拼接。然后去注册registerAction
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)
}
if (store._devtoolHook) {
return res.catch(err => {
store._devtoolHook.emit('vuex:error', err)
throw err
})
} else {
return res
}
})
}
我们看到在执行handler.call的时候,对应的context有很多参数,这也就是官网提到的

3.其次就是来看registerGetter。
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
)
}
}
registerGetter和其他的就有些不一样之处了。getters对应的就不是一个数组了而是一个函数,wrappedGetter。返回的是一个rawGetter。
以上呢我们就知道了如何把action,getter,mutation进行一个注册。其实整个呢其实就是构造了一个树仓。之后再进行数据处理的时候我们就能清晰的知道如何去对应的处理。
resetStoreVM 的实现
resetStoreVM(this, state) 这个时候我们会把this._modules.root.state作为参数进行传入。
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
computed[key] = () => 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
store._vm = new Vue({
data: {
$$state: state
},
computed
})
Vue.config.silent = silent
// enable strict mode for new vm
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())
}
}
首先给我们store定义了一个public getters,然后根据store._wrappedGetters拿到去计算拿到这个store.getters。然后进行遍历这个wrappedGetters,下面呢store._vm=new Vue,这个呢是利用vue做一个响应式,里面传入了data和computed,?state=state,后者state是传入的一个参数,这就是我们访问module.state就能访问到store.state,当我们访问每一个getters的时候,返回一个store._vm[key],通过计算属性返回fn(store)的计算结果。在这里面呢建立了state和getter的依赖关系。 最后呢就是说当我们再次去执行这个resetStoreVm,我们会将把之前的store.vm拿到进行保留,然后将之前的进行销毁,然后再重新建立它的store.vm。
以上是整个实例化的一个过程。store呢就是一个数据仓库,为了更好的管理呢,我们将一个大的store拆成了一些modules,整个modules是一个树形结构。每个module又分别定义了state,getters,mutations,actions,然后通过递归遍历模块的方式完成了它们的初始化。这个时候我们的store就建立起来了。
我们来看一下几个语法糖
mapState实现
export const mapState = normalizeNamespace((namespace, states) => {
const res = {}
normalizeMap(states).forEach(({ key, val }) => {
res[key] = function mappedState () {
let state = this.$store.state
let getters = this.$store.getters
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapState', namespace)
if (!module) {
return
}
state = module.context.state
getters = module.context.getters
}
return typeof val === 'function'
? val.call(this, state, getters)
: state[val]
}
// mark vuex getter for devtools
res[key].vuex = true
})
return res
})
从这段代码中我们看到mapState 的返回值是通过normalizeNamespace函数执行的结果。 现在我们看一下这个函数的
/**
* Return a function expect two param contains namespace and map. it will normalize the namespace and then the param's function will handle the new namespace and the map.
* @param {Function} fn
* @return {Function}
*/
function normalizeNamespace (fn) {
return function (namespace, map) {
if (typeof namespace !== 'string') {
map = namespace;
namespace = '';
} else if (namespace.charAt(namespace.length - 1) !== '/') {
namespace += '/';
}
return fn(namespace, map)
}
}
我们可以看到return中包含两个值,一个是namespace一个是map, 判断如果没有传namespace值,就把namespace赋值给map,否则两个参数都有的情况下,那么,namespace的最后一位如果不是一位的话,就自动添加一个'/', 实际上呢就是
export default {
name:'App',
computed:{
...mapState([
'count'
]),
...mapState('a',{
aCount:'count'
})
}
}
这里面的a后面加不加'/'都无所谓了。 其实这个函数主要是对namespace和map做一下处理。最后执行一个fn,这个时候我们去看一开始贴入的mapState的代码。 在函数中呢,执行了一个normalizeMap,把states传入,这个states呢就是

function normalizeMap (map) {
return Array.isArray(map)
? map.map(key => ({ key, val: key }))
: Object.keys(map).map(key => ({ key, val: map[key] }))
}
这个函数也就是map而言,支持两种类型,一种是数组,一种是对象,都是返回个key,value的形式。如果你是数组的话,就直接调用数组的map方法,例如:
normalizeMap([1, 2, 3]) => [ { key: 1, val: 1 }, { key: 2, val: 2 }, { key: 3, val: 3 } ],
如果是一个对象呢,就用对象的keys,例如:
normalizeMap({a: 1, b: 2, c: 3}) => [ { key: 'a', val: 1 }, { key: 'b', val: 2 }, { key: 'c', val: 3 } ]
normalizeMap函数处理完之后对结果进行遍历,如果namespace没有值的情况下,对val值进行一个判断,如果不是一个函数,就直接把值返回去,如果namespace有值的情况下,根据getModuleBynamespace这个方法去拿到这个module的值,这个方法很简单
function getModuleByNamespace (store, helper, namespace) {
var module = store._modulesNamespaceMap[namespace];
if (!module) {
console.error(("[vuex] module namespace not found in " + helper + "(): " + namespace));
}
return module
}
根据namespace,通过store._modulesNamespaceMap去拿到,举个示例:在初始化阶段呢,就会构造这个Module,

找到这个module就会进行返回,这个时候state和getters的值就是会module.context.state和module.context.getters, 也就是我们这个local,我们可以去看下这个local的源码,在vuex/src/store.js中,
const local = module.context = makeLocalContext(store, namespace, path)
在makeLocalContext函数中呢,
Object.defineProperties(local, {
getters: {
get: noNamespace
? () => store.getters
: () => makeLocalGetters(store, namespace)
},
state: {
get: () => getNestedState(store.state, path)
}
})
return local
也定义了local的getters和state,也就是说我们可以访问到local中的数据了,

也就是a模块下的内容,这个aCount也就是对应到模块A下的aCount。 以上呢就是我们mapState所做的事情。
mapGetters
mapGetters其实和mapState非常的类似,也是通过normalizeNamespace函数来执行,将我们的getters传入到normalizeMap中将返回值进行遍历,val值也是可以是拼接出来的,



mapMutations
export const mapMutations = normalizeNamespace((namespace, mutations) => {
const res = {}
normalizeMap(mutations).forEach(({ key, val }) => {
res[key] = function mappedMutation (...args) {
// Get the commit method from store
let commit = this.$store.commit
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapMutations', namespace)
if (!module) {
return
}
commit = module.context.commit
}
return typeof val === 'function'
? val.apply(this, [commit].concat(args))
: commit.apply(this.$store, [val].concat(args))
}
})
return res
})
我们可以看一下,mapMutations的这段代码和mapState的很相似,在执行这个函数的时候会把mutations做一次normalizeMap,变成这个key,val的形式,也会去判断namespace是否存在,如果没有的话,commit就是我们这个$store.commit,最终去直接调用commit的方法,如果有的话,就去getModuleNamespace方法去找到对应的module,然后找到module.context,局部的上下文找到对应的commit方法,再去提交。
mapActions
export const mapActions = normalizeNamespace((namespace, actions) => {
const res = {}
normalizeMap(actions).forEach(({ key, val }) => {
res[key] = function mappedAction (...args) {
// get dispatch function from store
let dispatch = this.$store.dispatch
if (namespace) {
const module = getModuleByNamespace(this.$store, 'mapActions', namespace)
if (!module) {
return
}
dispatch = module.context.dispatch
}
return typeof val === 'function'
? val.apply(this, [dispatch].concat(args))
: dispatch.apply(this.$store, [val].concat(args))
}
})
return res
})
mapActions的这个方法呢也是一样的,只不过换成了dispatch。找到了对应的dispatch,执行相应的dispatch。
这些呢就是我们所谓的语法糖,对原有语法的一些增强,让我们用更少的代码去实现同样的功能。这也是让我们学习到了一点,今后在设计一些js库的时候,从api设计角度中应该学习的方向。