Vuex 插件目录
// src/vuex/index.js// 容器初始化class Store {}// 插件安装逻辑:当 Vue.use(Vuex) 时执行const install = () => {}export default {Store,install}
模块化设计
将 Store 类和 install 方法抽离,使得 index.js 文件仅用于方法聚合并导出
// src/vuex/store.js// 容器初始化class Store {}// 插件安装逻辑:当 Vue.use(Vuex) 时执行const install = () => {}
// src/vuex/index.jsimport { Store, install } from './store';export default {Store,install}
install 插件安装逻辑
install 方法会传入 vue 的构造函数
// 容器初始化export class Store {}// 导出传入的 Vue 的构造函数,供插件内部的其他文件使用export let Vue;/*** 插件安装逻辑:当 Vue.use(Vuex) 时执行* @param {*} _Vue Vue 的构造函数*/export const install = (_Vue) => {Vue = _Vue;}
new Vue 时会注入 store 容器实例,同时通过全局混入,为所有组件添加 store 属性
// src/vuex/store.js/*** 将根组件中注入 store 实例,混入到所有子组件上* @param {*} Vue*/export default function applyMixin(Vue) {// 通过 beforeCreate 生命周期,在组件创建前,实现全局混入Vue.applyMixin({beforeCreate: vuexInit, // vuexInit 为初始化混入逻辑})}function vuexInit() {// $options 时在创建 vue 实例时传入的参数const options = this.$options;// 如果选项中拥有store属性,说明是根实例;其他情况都是子实例if (options.store) {// 为根实例添加 $store 属性,指向 store 实例this.$store = options.store;} else if (options.parent && options.parent.$store) {// 儿子可以通过父亲拿到 $store 属性,放到自己身上继续给儿子this.$store = options.parent.$store;}}
export const install = (_Vue) => {Vue = _Vue;applyMixin(Vue);}
State 状态的实现
State 状态的响应式:使用 vue 实例来保存 state
// 容器初始化export class Store {constructor(options) { // options:{state, mutation, actions}const state = options.state; // 获取 options 选项中的 state 对象// 响应式数据:new Vue({data})// vuex 中 state 状态的响应式是借助了 Vue 来实现的this._vm = new Vue({data: {// 在 data 中,默认不会将以$开头的属性挂载到 vm 上$$state: state // $$state 对象将通过 defineProperty 进行属性劫持}})}// 相当于 Object.defineProperty({}) 中的 getterget state() { // 对外提供属性访问器:当访问state时,实际是访问 _vm._data.$$statereturn this._vm._data.$$state;}}
getters 方法的实现
将选项中的 getters 方法,保存到 store 实例中的 getters 对象中
借助 Vue 原生 computed,实现 Vuex 中 getters 的数据缓存功能
// 容器初始化export class Store {constructor(options) { // options:{state, mutation, actions}const state = options.state; // 获取 options 选项中的 state 对象// 获取 options 选项中的 getters 对象:内部包含多个方法const getters = options.getters;// 声明 store 实例中的 getters 对象this.getters = {};// 将 options.getters 中的方法定义到计算属性中const computed = {};// 将用户传入的 options.getters 属性中的方法,转变成为 store 实例中的 getters 对象上对应的属性Object.keys(getters).forEach(key => {// 将 options.getters 中定义的方法,放入计算属性 computed 中,即定义在 Vue 的实例 _vm 上computed[key] = () => {return getters[key](this.state);}// 将 options.getters 中定义的方法,放入store 实例中的 getters 对象中Object.defineProperty(this.getters, key, {// 取值操作时,执行计算属性逻辑get: () => this._vm[key]})})// 响应式数据:new Vue({data})// vuex 中 state 状态的响应式是借助了 Vue 来实现的this._vm = new Vue({data: {// 在 data 中,默认不会将以$开头的属性挂载到 vm 上$$state: state // $$state 对象将通过 defineProperty 进行属性劫持},computed // 将 options.getters 定义到 computed 实现数据缓存})}}
mutations 的实现
将 options.mutations 中定义的方法,绑定到 store 实例中的 mutations 对象 ```typescript // 声明 store 实例中的 mutations 对象 this.mutations = {}; // 获取 options 选项中的 mutations 对象 const mutations = options.mutations;
// 将 options.mutations 中定义的方法,绑定到 store 实例中的 mutations 对象 Object.keys(mutations).forEach(key => { // payload:commit 方法中调用 store 实例中的 mutations 方法时传入 this.mutations[key] = (payload) => mutationskey; });
/**
* 通过 type 找到 store 实例的 mutations 对象中对应的方法,并执行* 用户可能会解构使用{ commit }, 也有可能在页面使用 $store.commit,* 所以,在实际执行时,this是不确定的,{ commit } 写法 this 为空,* 使用箭头函数:确保 this 指向 store 实例;* @param {*} type mutation 方法名* @param {*} payload 载荷:值或对象*/
this.commit = (type, payload) => { this.mutationstype; }
<a name="jgvhh"></a># Actions 的实现```typescript// 声明 store 实例中的 actions 对象this.actions = {};// 获取 options 选项中的 actions 对象const actions = options.actions;// 将 options.actions 中定义的方法,绑定到 store 实例中的 actions 对象Object.keys(actions).forEach(key => {// payload:dispatch 方法中调用 store 实例中的 actions 方法时传入this.actions[key] = (payload) => actions[key](this, payload);})/*** 通过 type 找到 store 实例的 actions 对象中对应的方法,并执行* 用户可能会解构使用{ dispatch }, 也有可能在页面使用 $store.dispatch,* 所以,在实际执行时,this 是不确定的,{ dispatch } 写法 this 为空,* 使用箭头函数:确保 this 指向 store 实例;* @param {*} type action 方法名* @param {*} payload 载荷:值或对象*/this.dispatch = (type, payload) => {// 执行 actions 对象中对应的方法,并传入 payload 执行this.actions[type](payload);}
模块收集
将模块对象格式化成为一颗“模块树”
模块的树型结构-模块树
数据格式化,即构建“模块树”的逻辑,就是递归的将子模块注册到父模块中。和 AST 语法树的构建过程相似:
- 首先,层级上有父子关系,理论上是都支持无限递归的;
其次,由于采用深度优先的递归方式,需要使用栈结构来保存层级关系(相当于地图);
// 模块树对象{_raw: '根模块',_children:{moduleA:{_raw:"模块A",_children:{moduleC:{_raw:"模块C",_children:{},state:'模块C的状态'}},state:'模块A的状态'},moduleB:{_raw:"模块B",_children:{},state:'模块B的状态'}},state:'根模块的状态'}
模块收集的实现
深度遍历构建树型结构 ```typescript /**
- 模块收集操作
- 处理用户传入的 options 选项
将子模块注册到对应的父模块上 */ class ModuleCollection { constructor(options) {
// 从根模块开始,将子模块注册到父模块中// 参数 1:数组,用于存储路径,标记模块树的层级关系this.register([], options);
}
/**
- 将子模块注册到父模块中
- @param {*} path 数据类型,当前待注册模块的完整路径
@param {} rootModule 当前待注册模块对象 / register(path, rootModule) { // 格式化,并将当前模块,注册到对应父模块上,构建 Module 对象 let newModule = {
_raw: rootModule, // 当前模块的完整对象_children: {}, // 当前模块的子模块state: rootModule.state // 当前模块的状态
}
// 根模块时:创建模块树的根对象 if (path.length == 0) {
this.root = newModule;
} else {
// 非根模块时,将当前模块,注册到对应父模块上// 逐层找到当前模块的父亲(例如:path = [a,b,c,d])let parent = path.slice(0, -1).reduce((memo, current) => {//从根模块中找到a模块;从a模块中找到b模块;从b模块中找到c模块;结束返回c模块即为d模块的父亲return memo._children[current];}, this.root);// 将d模块注册到c模块上parent._children[path[path.length - 1]] = newModule;
}
// 若当前模块存在子模块,继续注册子模块 if (rootModule.modules) {
// 采用深度递归方式处理子模块Object.keys(rootModule.modules).forEach(moduleName => {let module = rootModule.modules[moduleName];// 将子模块注册到对应的父模块上// 1.path:待注册子模块的完整路径,当前父模块 path 拼接子模块名 moduleName// 2. module:当前待注册子模块对象this.register(path.concat(moduleName), module);})
} } }
export default ModuleCollection;
<a name="LVklz"></a># 模块安装> 递归“模块树”并将所有模块的 getter、mutation、action 定义到当前 store 实例中- 从根模块开始进行模块安装,递归处理格式化后的“模块树”对象- 根据模块名称,将全部子模块定义到根模块上,同时将状态合并到根模块上```typescript/*** 安装模块* @param {*} store 容器* @param {*} rootState 根状态* @param {*} path 所有路径* @param {*} module 格式化后的模块对象*/const installModule = (store, rootState, path, module) => {// 遍历当前模块中的 actions、mutations、getters// 将它们分别定义到 store 中的 _actions、_mutations、_wrappedGetters;// 遍历 mutationmodule.forEachMutation((mutation, key) => {// 处理成为数组类型:每个 key 可能会存在多个需要被处理的函数store._mutations[key] = (store._mutations[key] || []);// 向 _mutations 对应 key 的数组中,放入对应的处理函数store._mutations[key].push((payload) => {// 执行 mutation,传入当前模块的 state 状态mutation.call(store, module.state, payload);})})// 遍历 actionmodule.forEachAction((action, key) => {store._actions[key] = (store._actions[key] || []);store._actions[key].push((payload) => {action.call(store, store, payload);})})// 遍历 gettermodule.forEachGetter((getter, key) => {// 注意:getter 重名将会被覆盖store._wrappedGetters[key] = function () {// 执行对应的 getter 方法,传入当前模块的 state 状态,返回执行结果return getter(module.state)}})// 遍历当前模块的儿子module.forEachChild((child, key) => {// 递归安装/加载子模块installModule(store, rootState, path.concat(key), child);})}// 容器的初始化export class Store {constructor(options) {const state = options.state;this._actions = {};this._mutations = {};this._wrappedGetters = {};this._modules = new ModuleCollection(options);installModule(this, state, [], this._modules.root);}// ...}
流程梳理
- 当项目引用并注册 vuex 插件时,即 Vuex.use(vuex),将执行 Vuex 插件中的 install 方法;
- install 方法,接收外部传入的 Vue 实例,并通过 Vue.mixin 实现 store 实例的全局共享;
- 项目中通过 new Vuex.Store(options) 配置 vuex 并完成 store 状态实例的初始化;
- 在 Store 实例化阶段时,将会对 options 选项进行处理,此时完成 Vuex 模块收集和安装操作;
在 new Vue 初始化时,将 store 实例注入到 vue 根实例中(此时的 store 实例已实现全局共享);
State 状态安装
对“模块树”中状态的安装,就是将所有子模块上的 State 状态,挂载到对应父模块的 State 状态上:
处理范围:子模块,即 path.length > 0 时,执行状态安装逻辑
- 处理逻辑:将子模块的状态 module.state,挂载到其父模块的状态上
通过
Vue.setAPI 向父模块状态中添加子模块状态,以此实现对象新增属性为响应式数据/*** 安装模块* @param {*} store 容器* @param {*} rootState 根状态* @param {*} path 所有路径* @param {*} module 格式化后的模块对象*/const installModule = (store, rootState, path, module) => {// 处理子模块:将子模块上的状态,添加到对应父模块的状态中;if(path.length > 0){// 从根状态开始逐层差找,找到当前子模块对应的父模块状态let parent = path.slice(0, -1).reduce((memo, current)=>{return memo[current]}, rootState)// 支持 Vuex 动态添加模块,将新增状态直接定义成为响应式数据;Vue.set(parent, path[path.length-1], module.state);}}
Vuex 插件的开发
```typescript import Vue from ‘vue’; import Vuex from ‘vuex’;
// 引入 Vuex 日志插件 logger import logger from ‘vuex/dist/logger’
Vue.use(Vuex);
const store = new Vuex.Store({ // 在 plugins 数组中可以注册多个 Vuex 插件,插件的执行是串行顺序执行的 plugins: [ logger(), // 日志插件:导出的 logger 是一个高阶函数 ], }); export default store;
1. 创建一个 Vuex 插件,最终导出一个高阶函数(在 plugin 数组中进行插件注册);1. Vuex 的 Store 类提供的订阅方法 store.subscribe:当 mutation 方法触发时被执行;1. Vuex 的 Store 类提供的状态替换方法 store.replaceState:能够更新 Vuex 中的状态;```typescriptimport Vue from 'vue';import Vuex from 'vuex';Vue.use(Vuex);// vuex-persists 插件实现function persists() {return function (store) {console.log("----- persists 插件执行 -----")// 取出本地存储的状态let data = localStorage.getItem('VUEX:STATE');if (data) {console.log("----- 存在本地状态,同步至 Vuex -----")// 如果存在,使用本地状态替换 Vuex 中的状态store.replaceState(JSON.parse(data));}// subscribe:由 vuex 提供的订阅方法,当触发 mutation 方法时被执行;store.subscribe((mutation, state) => {console.log("----- 进入 store.subscribe -----")localStorage.setItem('VUEX:STATE', JSON.stringify(state));})}}const store = new Vuex.Store({plugins: [logger(), // 日志插件:导出的 logger 是一个高阶函数persists() // 持久化插件:vuex-persists]});export default store;
插件机制内部的实现:
- store.subscribe:状态变更时的订阅回调功能;
该函数和众多插件实现类似
// 调用时传入一个队列中subscribe(fn) {this._subscribes.push(fn);}// 状态改变时,从队列取出触发store._subscribes.forEach(fn => {fn(mutation, rootState);})
