一,前言
上一篇,主要介绍了 Vuex 响应式数据和缓存的实现,主要涉及以下几个点:
- Vuex 的响应式实现原理;
- 响应式核心方法 resetStoreVM;
- commit 和 dispatch 的处理;
本篇,继续介绍 Vuex-namespaced 命名空间的实现;
二,前文梳理
之前几篇,完成了 Vuex 的模块收集、模块安装、响应式数据和缓存;
- 模块收集:通过 ModuleCollection 类,对 options 选项进行处理,构建“模块树”;
- 模块安装:处理“模块树”,将各模块中 mutation、action、getter,注册到 store 实例上:_actions、_mutations、_wrappedGetters;将各模块的 State 状态通过
Vue.set
注册到根状态; - 响应式数据和缓存:通过
resetStoreVM
在 store 实例中创建一个 Vue 实例, 借助 Vue 能力将 state、getter 通过 data、computed 进行定义,实现数据的响应式和缓存;
接下来,继续实现 namespaced 命名空间功能;
三,命名空间的使用
1,命名空间介绍
前面已经简单介绍过:在子模块对象中添加 namespaced:true
,为模块开启命名空间功能;
开启命名空间功能,相当于为每个模块添加独立的作用域,实现模块间状态和事件的隔离;
2,命名空间的使用
使用官方的 Vuex 插件,以“根模块->模块 A->模块 C”的父子模块关系为例,测试多种使用场景:
1)模块 A 和模块 C 都启用了 namespaced 命名空间:
此时,模块 C 的 changeNum:moduleA/moduleC/changeNum;
2)模块 A 启用了命名空间,但模块 C 没启用命名空间:
此时,模块 C 的 changeNum:moduleA/changeNum;
3)模块 C 启用了命名空间,但模块 A 没启用命名空间:
此时,模块 C 的 changeNum:moduleC/changeNum;
三,命名空间的实现
1,命名空间的逻辑分析
根据测试情况分析命名空间的划分规则:
在模块注册阶段,会通过类似发布订阅的方式将各模块中的 action、mutation 进行手机并注册,需要根据模块是否开启命名空间状态,为模块拼接命名空间前缀;
所以,可以统一理解为,在事件订阅时,为事件添加对应命名空间标识即可;
2,命名空间的代码实现
在 installModule 模块安装阶段,获取当前模块的命名空间:
// src/vuex/store.js#installModule
/**
* 安装模块
* @param {*} store 容器
* @param {*} rootState 根状态
* @param {*} path 所有路径
* @param {*} module 格式化后的模块对象
*/
const installModule = (store, rootState, path, module) => {
// 根据当前模块的 path 路径,拼接当前模块的命名空间标识
let namespace = store._modules.getNamespaced(path);
}
为 ModuleCollection 类添加 getNamespaced 方法:
// src/vuex/module/module-collection.js
class ModuleCollection {
constructor(options) {
this.register([], options);
}
/**
* 根据当前模块的 path 路径,拼接当前模块的命名空间标识
* @param {*} path
* @returns
*/
getNamespaced(path) {
let root = this.root;
// 从根模块开始,逐层处理子模块,拼接命名空间标识
return path.reduce((str, key) => {
// 从根模块查找当前子模块
root = root.getChild(key);
// 若子模块启用命名空间,拼接命名空间标识并返回结果继续处理
return str + (root.namespaced ? key + '/' : '')
}, '');
}
}
测试getNamespaced
获取命名空间:
模块 A、B 都开启命名空间的情况:
模块 A 不开启命名空间,模块 C 开启命名空间的情况:
结论与官方 Vuex 插件相同;
拿到了命名空间后,就需要在注册事件时,为其添加对应的命名空间标识;
在模块安装注册时,为事件添加命名空间标识:
// src/vuex/store.js#installModule
/**
* 安装模块
* @param {*} store 容器
* @param {*} rootState 根状态
* @param {*} path 所有路径
* @param {*} module 格式化后的模块对象
*/
const installModule = (store, rootState, path, module) => {
// 根据当前模块的 path 路径,拼接当前模块的命名空间标识
let namespace = store._modules.getNamespaced(path);
if (path.length > 0) {
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current]
}, rootState)
Vue.set(parent, path[path.length - 1], module.state);
}
// 遍历 mutation
module.forEachMutation((mutation, key) => {
// 添加命名空间标识: namespace + key
store._mutations[namespace + key] = (store._mutations[namespace + key] || []);
store._mutations[namespace + key].push((payload) => {
mutation.call(store, module.state, payload);
})
})
// 遍历 action
module.forEachAction((action, key) => {
// 添加命名空间标识: namespace + key
store._actions[namespace + key] = (store._actions[namespace + key] || []);
store._actions[namespace + key].push((payload) => {
action.call(store, store, payload);
})
})
// 遍历 getter
module.forEachGetter((getter, key) => {
// 添加命名空间标识: namespace + key
store._wrappedGetters[namespace + key] = function () {
return getter(module.state);
}
})
module.forEachChild((child, key) => {
installModule(store, rootState, path.concat(key), child);
})
}
3,命名空间效果测试
测试示例:
<template>
<div id="app">
<br> 根模块测试: <br>
商品数量: {{this.$store.state.num}} 个<br>
商品单价: 10 元<br>
订单金额: {{this.$store.getters.getPrice}} 元<br>
<button @click="$store.commit('changeNum',5)">同步更新:数量+5</button>
<button @click="$store.dispatch('changeNum',-5)">异步更新:数量-5</button>
<br> 子模块测试: <br>
A 模块-商品数量: {{this.$store.state.moduleA.num}} 个<br>
B 模块-商品数量: {{this.$store.state.moduleB.num}} 个<br>
C 模块-商品数量: {{this.$store.state.moduleA.moduleC.num}} 个<br>
<button @click="$store.commit('moduleA/changeNum',5)">A 模块-同步更新:数量+5</button>
<button @click="$store.commit('moduleB/changeNum',5)">B 模块-同步更新:数量+5</button>
<button @click="$store.commit('moduleA/moduleC/changeNum',5)">C 模块-同步更新:数量+5</button>
</div>
</template>
点击按钮前(页面初始化完成)
点击按钮后(更新完成)
- 上边区域对根模块的测试:
点击同步/异步更新按钮,通过$store.commit('changeNum',5)
、$store.dispatch('changeNum',-5)
操作,只能够影响到根模块中 mutation、action 对应的 changeNum 事件,不再对子模块产生影响;
- 下边区域对子模块的测试:
点击同步更新按钮,通过$store.commit('moduleA/changeNum',5)
操作,会根据命名空间标识查找对应模块下的changeNum
事件;
这样,就实现了不同子模块间状态和事件操作的隔离;
四,核心逻辑梳理
- 在 ModuleCollection 模块收集类中,提供根据 path 获取命名空间标识的能力:
getNamespaced(path)
; - 在 installModule 模块安装时,通过调用
getNamespaced(path)
获取当前模块的命名空间标识; - 在安装/注册
mutation
、action
、getter
时,为对应的事件添加(拼接)上命名空间标识;
这样,就实现了 Vuex 命名空间 namespaced 功能,即:根模块与各子模块中定义的事件完全独立互不影响;
五,结尾
本篇,主要介绍了 Vuex-namespaced 命名空间的实现,主要涉及以下几个点:
- 命名空间的介绍和使用;
- 命名空间的逻辑分析与代码实现;
- 命名空间核心流程梳理;
下一篇,继续介绍 Vuex 插件的实现