一,前言
上一篇,主要介绍了 Vuex 的 State 状态安装,主要涉及以下几个点:
- State 状态的安装逻辑;
- 两个核心问题的思路;
- 代码实现以及执行情况分析;
本篇,继续介绍 Vuex 模块相关概念:Vuex 响应式数据和缓存的实现;
二,前文梳理
- 第六篇,完成了 Vuex 的模块收集;
- 第七篇、第八篇,两篇完成了 Vuex 的模块安装;
下一步,实现 Vuex 响应式数据和缓存;
三,Vuex 的响应式实现原理
前面,简易的单层 Vuex 实现中,通过借助 Vue 实例与数据代理实现了响应式数据:
export class Store {
constructor(options) {
// 获取 options 选项中的 state 对象
const state = options.state;
// 获取 options 选项中的 getters 对象:内部包含多个方法
const getters = options.getters;
// 声明 store 实例中的 getters 对象
this.getters = {};
// 将 options.getters 中的方法定义到计算属性中
const computed = {}
// 页面通过“{{this.$store.getters.getPrice}}”取值,取的是 getters 对象中的属性
// 所以,需要将将用户传入的 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, {
// 旧:// 取值操作时,执行 options 中对应的 getters 方法(添加computed后废弃,使用逻辑)
// get: () => options.getters[key](this.state)
// 新:取值操作时,执行计算属性逻辑
get: () => this._vm[key]
})
});
// 响应式数据:new Vue({data})
this._vm = new Vue({
data: {
// 在 data 中,默认不会将以$开头的属性挂载到 vm 上
$$state: state // $$state 对象将通过 defineProperty 进行属性劫持
},
computed // 将 options.getters 定义到 computed 实现数据缓存
})
}
全部 getters 的安装结果:_wrappedGetters
;
全部 state 的安装结果:state
;
所以,使用 _wrappedGetters
和state
创建 Vue 实例,将 State 状态和 getters 都定义在当前 vm 实例上;
创建 resetStoreVM 方法:重置 Store 容器对象的 vm 实例:
- 传入 this:this 中包含
_wrapperGetters
全部的getter
方法; - 传入 state:
rootState
根模块状态,包含全部模块的状态;
借助 Vue 的响应式,为 store 实例上添加一个 Vue 实例_vm
,通过计算属性实现 _wrapperGetters
中的 getters
的缓存效果,并将 state 定义到 data 中实现状态的响应式能力;
四,响应式实现核心方法:resetStoreVM
根据以上分析,借助 Vue 中 data 响应式能力与,computed计算属性提供的缓存能力,实现 Vuex 数据响应式及缓存效果;
在 src/vuex/store.js 中,创建 resetStoreVM 方法:
// src/vuex/store.js#resetStoreVM
/**
* 重置 Store 容器对象的 vm 实例
* @param {*} store store实例,包含 _wrappedGetters 即全部的 getter 方法;
* @param {*} state 根状态,在状态安装完成后包含全部模块状态;
*/
function resetStoreVM(store, state) {
const computed = {}; // 定义 computed 计算属性
store.getters = {}; // 定义 store 容器实例中的 getters
// 遍历 _wrappedGetters 构建 computed 对象并进行数据代理
forEachValue(store._wrappedGetters, (fn, key) => {
// 构建 computed 对象,后面借助 Vue 计算属性实现数据缓存
computed[key] = () => {
return fn();
}
// 数据代理:将 getter 的取值代理到 vm 实例上,到计算数据取值
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
});
})
// 使用 state 根状态 和 computed 创建 vm 实例,成为响应式数据
store._vm = new Vue({
// 借助 data 使根状态 state 成为响应式数据
data: {
$$state: state
},
// 借助 computed 计算属性实现数据缓存
computed
});
}
备注:
- 计算属性的所有属性,都会被放到 vm 实例上,所以可以通过
store._vm[key]
取到; - 当取值时,才会走
get: () => store._vm[key]
;此时,store._vm = new Vue
已经执行完成;
调用 resetStoreVM 实现 Vuex 状态响应式及 getter 缓存效果:
export class Store {
constructor(options) {
const state = options.state;
this._actions = {};
this._mutations = {};
this._wrappedGetters = {};
// 1,模块收集:options 格式化 -> Vuex 模块树
this._modules = new ModuleCollection(options);
console.log("格式化后的模块树对象", this._modules);
// 2,模块安装:
installModule(this, state, [], this._modules.root);
// 3,将 state 状态、getters 定义在当前的 vm 实例上
resetStoreVM(this, state);
}
// 对外提供属性访问器:当访问 state 时,实际是访问 _vm._data.$$state
get state() {
return this._vm._data.$$state
}
}
- 当
new Vuex.Store(options)
时,Store构造函数执行模块收集、模块安装、创建 vm 实例; computed
计算属性,也就是getter
,将被挂载到vm
实例上;state
也会被代理到vm
实例上;- 直接访问
state
状态时,会通过属性访问器,相当于直接访问vm
实例内部_data.$$state
属性; - 直接访问
store.getters
时,会被getter
代理到store
实例中的_vm
实例上取值;
五,commit 和 dispatch 的处理
视图部分:
// src/App.vue
<template>
<div id="app">
商品数量: {{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>
</div>
</template>
当点击 commit 和 dispatch 时,会进入 store 类中 commit 和 dispatch 方法:
export class Store {
constructor(options) {
const state = options.state;
this._actions = {};
this._mutations = {};
this._wrappedGetters = {};
// 1,模块收集:options 格式化 -> Vuex 模块树
this._modules = new ModuleCollection(options);
// 2,模块安装:
installModule(this, state, [], this._modules.root);
// 3,将 state 状态、getters 定义在当前的 vm 实例上
resetStoreVM(this, state);
}
get state() {
return this._vm._data.$$state
}
/**
* 通过 type 找到 store 实例的 mutations 对象中对应的方法,并执行
* 用户可能会解构使用{ commit }, 也有可能在页面使用 $store.commit,
* 所以,在实际执行时,this 是不确定的,{ commit } 写法 this 为空,
* 使用箭头函数:确保 this 指向 store 实例;
* @param {*} type mutation 方法名
* @param {*} payload 载荷:值或对象
*/
commit = (type, payload) => {
// 旧:执行 mutations 对象中对应的方法,并传入 payload 执行
// this.mutations[type](payload)
// 新:不再去 mutations 对象中查找,直接在 _mutations 中找到 type 对应的数组,依次执行
this._mutations[type].forEach(mutation=>mutation.call(this, payload))
}
/**
* 通过 type 找到 store 实例的 actions 对象中对应的方法,并执行
* 用户可能会解构使用{ dispatch }, 也有可能在页面使用 $store.dispatch,
* 所以,在实际执行时,this 是不确定的,{ dispatch } 写法 this 为空,
* 使用箭头函数:确保 this 指向 store 实例;
* @param {*} type action 方法名
* @param {*} payload 载荷:值或对象
*/
dispatch = (type, payload) => {
// 旧:执行 actions 对象中对应的方法,并传入 payload 执行
// this.actions[type](payload)
// 新:不再去 actions 对象中查找,直接在 _actions 中找到 type 对应的数组,依次执行
this._actions[type].forEach(action=>action.call(this, payload))
}
}
通过类型,找到 commit、dispatch 对应的数组(数组中存放所有待执行的方法),循环执行即可;
这里使用到了发布订阅模式,先将所有模块中的 mutation 和 action 放到两个数组中,调用时到当前对象中找到对应的数组并依次执行
这样,数据是如何放入,如何执行的就清晰了,后续进行命名空间的处理;
测试效果:
当点击同步、异步更新时,进入 commit、dispatch 方法,通过 type 类型找到对应数组并依次执行;
备注:虽然 Vuex 模块的定义中 namespaced 命名空间是启用的,但当前代码还尚未支持;
六,结尾
本篇,主要介绍了 Vuex 响应式数据和缓存的实现,主要涉及以下几个点:
- Vuex 的响应式实现原理;
- 响应式核心方法 resetStoreVM;
- commit 和 dispatch 的处理;
下一篇,继续介绍 Vuex 命名空间 namespaced 的实现;