时隔一年了,又从新把vue源码拿起来了,准备记录下来,也算是对本身知识的一个巩固。vue
vuex做为vue的一个生态插件,也是支持npm发布的,当咱们import的时候呢,执行的是vuex/dist/vuex.esm.js这里面的代码 vuex
export default { Store, install, version: '__VERSION__', mapState, mapMutations, mapGetters, mapActions, createNamespacedHelpers } 复制代码
能够知道当咱们import vuex的时候,实际上就会返回这样的一个对象,因此咱们vuex上也有store这个对象,当咱们使用Vue.use(Vue)的时候,就会执行这个install。咱们来看下这个install的源码。在src/store.js中npm
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中api
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 }) 复制代码
对于Store的定义呢是在src/store.js中,咱们来分析一下store的这个构造函数的执行逻辑promise
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.浏览器
在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,上个图瞅一眼bash
下面咱们分析下installModule的实现markdown
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(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就创建起来了。
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其实和mapState很是的相似,也是经过normalizeNamespace函数来执行,将咱们的getters传入到normalizeMap中将返回值进行遍历,val值也是能够是拼接出来的,
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方法,再去提交。
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设计角度中应该学习的方向。