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.js
import { 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({}) 中的 getter
get state() { // 对外提供属性访问器:当访问state时,实际是访问 _vm._data.$$state
return 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;
// 遍历 mutation
module.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);
})
})
// 遍历 action
module.forEachAction((action, key) => {
store._actions[key] = (store._actions[key] || []);
store._actions[key].push((payload) => {
action.call(store, store, payload);
})
})
// 遍历 getter
module.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.set
API 向父模块状态中添加子模块状态,以此实现对象新增属性为响应式数据/**
* 安装模块
* @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 中的状态;
```typescript
import 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);
})