VUEX 3.x源码分析——5. 理解Module

这是对vuex3版本的源码分析。
本次分析会按以下方法进行:

  1. 按官网的使用文档顺序,围绕着某一功能点进行分析。这样不仅能学习优秀的项目源码,更能加深对项目的某个功能是如何实现的理解。这个对自己的技能提升,甚至面试时的回答都非常有帮助。
  2. 在围绕某个功能展开讲解时,所有不相干的内容都会暂时去掉,等后续涉及到对应的功能时再加上。这样最大的好处就是能循序渐进地学习,同时也不会被不相干的内容影响。省略的内容都会在代码中以…表示。
  3. 每段代码的开头都会说明它所在的文件目录,方便定位和查阅。如果一个函数内容有多个函数引用,这些都会放在同一个代码块中进行分析,不同路径的内容会在其头部加上所在的文件目录。

本章只讲解vuex中的Module,这也是vuex官网中“核心概念”的最后一个。
想了解vuex中的其他源码分析,欢迎参考我发布的下列文章:
VUEX 3 源码分析——1. 理解State
VUEX 3 源码分析——2. 理解Getter
VUEX 3 源码分析——3. 理解Mutations
VUEX3 源码分析——4. 理解Action
VUEX 3 源码分析——5. 理解Module
VUEX 3 源码分析——6. 理解命名空间namespace
VUEX 3 源码分析——7. 模块的动态注册和卸载

官方示例

  • 对于下面的代码示例,可以发现模块内部的state、getter、mutation、action的调用方式和之前未使用module分块时有所不同。
  • state的访问变成了store.state.moduleName
  • mutation 和 getter,接收的第一个参数是模块的局部状态对象。
  • action局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState
const moduleA = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}

const moduleB = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    a: moduleA,
    b: moduleB
  }
})

store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态

const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  },
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

module的初始化

  • 假设我们按上面示例代码的moduleA和moduleB的内容和结构进行Store的实例化,看下vuex的源码会如何处理。
  • 首先,遍历store初始化时定义的modules内容,调用register方法,这里传入的参数path = [‘moduleA’](path.concat(key) 逻辑),rawModule = 我们定义的moduleA的所有内容。
  • 通过 this.get 方法获得父模块。
  • 通过 父模块的 addChild 方法,将子模块赋值到父模块的_children中。
// store.js
export class Store {
    constructor(options = {}) {
         this._modules = new ModuleCollection(options)   
    }
    ...
}

// ./module/module-collection.js
export default calss 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) {
        ...
        if (path.length == 0) {
            this.root = rawModule        
        } 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)        
        })    
    }
}

// ./module/module.js

export default class Module {
	constructor(rawModule, runtime) {
		this._children = Object.create(null)
		...
	}
	addChild(key, module) {
		this._children[key] = module
	}
	getChild(key) {
		return this._children[key]
	}
}

installModule函数中对module的处理

  • 在installModule函数中,遍历根模块的_children,递归调用installModule。
  • 将子模块的state以子模块名为key,绑定到父模块的state中,这里就解释了为什么对子模块state是通过store.state.a或者store.state.b来访问的。
// store-util.js

// installModule(this, state, [], this._modules.root) in store.js
export function installModule(store, rootState, path, module, hot) {
	const isRoot = !path.length
	// 由于示例没有设置命名空间,所以所有的namespcae都为空字符串
	const namespace = store._modules.getNamespace(path)
	...
	// 将子模块的state绑定到父模块的state中
	if (!isRoot && !hot) {
		const parentState = getNestedState(rootState, path.slice(0, -1))
		const moduleName = path[path.length - 1]
		store._withCommit(() => {
			...
			parentState[moduleName] = module.state
		})
		
	}
	// 生成局部的包含state, getters等属性的对象
	const local = module.context = makeLocalContext(store, namespace, path)
	...
	module.forEachChild((child, key) => {
		installModule(store, rootState, path.concat(key), child, hot)
	})
}
  • 在每个模块中,都生成了一个local对象,这个对象有state,getters,commit和dispatch四个属性,不考虑命名空间namespace的情况下,getters,commit和dispatch都是对应store的getters,commit和dispatch,而state对应子模块的state。
./store-util.js
function makeLocalContext(store, namespace, path) {
	const noNamespace = namespace === '' // 这里的store实例没有使用命名空间,所以等于True
	const local = {
		dispatch: noNamespace ? store.dispatch : ...,
		commit: noNamespace ? store.commit : ...
		// 省略的内容是使用了命名空间时的逻辑
	}
	Object.defineProperties(local, {
		getters: {
			get: noNamespace ? () => store.getters : ...
		},
		state: {
			get: () => getNestedState(store.state, path)
		}
	})
}

export function getNestedState(state, path) {
	return path.reduce((state, key) => state[key], state)
}
  • 由于local中的state是当前子模块的局部状态,所以模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。但是分析 registerGetter 函数可以发现,getter除了接受的局部state作为第一个参数以外,还接受局部的getters以及全局的state和getters。
  • 而对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState。同时分析下面的 registerAction 函数可以发现,action和getter一样,也接受一些全局内容。
// ./store-util.js
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) // local.state是子模块的state
    })
}

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
    	)
    }
}

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
	})
}
  • 另外,由于未使用命名空间,即namespace为空字符串,假如局部模块定义的某个getter,mutation和action和全局定义的或者另一局部模块定义的名称相同,会发生一些问题。具体会发生什么问题,可以看这些内容各自的register函数源码就能明白,我在之前的文章中也有贴出,这里就不多解释了。
  • 这就是官网中描述的“默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。”
export function installModule (store, rootState, path, module, hot) {
	...
	// namespace = ""
	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)
  	})
}

为了避免模块与模块之间的一些命名问题,维持更高的封装度和复用性,尤其在项目复杂庞大的情况下,就需要使用官网说的 namespaced: true的方式使其成为带命名空间的模块
以上就是官网上Module的模块的具体状态相关源码,下一遍会分析Module中,使用了命名空间后会和现在的逻辑有什么不同。

  • 33
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值