「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
前言
在本文里,我提及的Vuex版本还是Vuex3,不是与Vue3相对应的Vuex4,因为Vue3的项目更多可能是用pinia替代Vuex4作为状态管理库,最近我也在学习pinia,等学成后,我再来单独写一篇关于pinia的。虽然Vue3现在成为了主流版本,但和Vue2相关的技术还是得会,毕竟公司的老项目还依旧是基于Vue2的。
首先vuex是什么?
我在官网里找到了关于Vuex的定义,如下图
有几个关键概念我加粗下,理解了这几个关键概念,Vuex这个Vue插件你就应该算是完全理解了。首先是个状态管理模式+库,然后采用的是集中式的管理方式,并用相应的规则保证状态变更的可预测性。
- 状态管理模式 其实从上面的Vuex的状态管理模式图就可以发现,首先数据流是单向的。从组件派发一个Action,Action里可组合复杂的业务逻辑后,再提交一个Mutation改变state状态值,状态值改变后触发组件的更新函数,重新渲染组件。
- 集中式 之所以称为集中式,因为所有组件的状态都集中交由Vuex的store类统一管理。
- 可预测 那为什么是可预测呢?因为在严格模式开启下,所有的state变更都只能通过Vuex提供的Commit操作,这就确保了状态的稳定性,既然确保每次更改前都有Commit操作,那我们就可以在状态改变前执行些日志打印操作或接入Devtools用于调试。所以Vuex是可预测的。
接下来是Vuex如何使用?
Vuex的简单使用只需要4步走:
- 引入vuex,并执行Vue.use(Vuex)。
- 导出一个new Vuex.Store(options) 实例,options传递的是一个选项对象,包含state、mutations、actions、getters等。
- 在new Vue(options)的时候,把Store实例作为选项之一传进去,为了以后把Store实例挂载到了当前vue实例的this上。
- 在组件中可直接通过this.store访问state、getters里的状态值;可通过this.store.commit(type)提交一个mutation同步改变state状态值;可通过this.store.dispatch(type)派发一个action处理复杂逻辑,可包含异步操作,最终通过commit改变state状态值。 详细的使用教程可到Vuex官网查阅
最后一起来手写实现
- 首先从Vuex使用的方式来看,导出去的Vuex是个对象,有install静态方法,有Store类用于管理状态。
- install方法里用Vue.mixin混入一个beforeCreate生命周期函数,在Vue原型里注册$store属性。
- state里的状态值变化,其依赖该状态值的组件也会重新渲染,所以state的状态值需要是响应式的;在Vuex里使用的是new Vue()的方式,把用户定义的state存入data选项的$$state属性,这样借助Vue就完成了state的响应式。
- 细节1,为什么要用$$state属性名,因为我们不想用户直接操作这个state,而且在Vue源码里data里的响应式数据会代理到实例上,美元符号开头的属性名则不会被代理,这样就可以把它藏得深一些。然后通过Store类的存取器来暴露state获取状态值,并且可阻止用户直接覆盖state。这是Vuex能可预测的第一层封印。
- 细节2,一开始我百思不得其解,为什么直接给state里的状态值赋值可以发出警告,而commit的方式却可以。后来在Vuex的源码里发现了答案,原来还是通过Vue定义一个用户watch,在每个state里的状态值改变的时候调用一个assert方法,assert内部会通过由commit调用引起的状态值变更,反之则报错。这是Vuex能可预测的第二层封印。
- Vuex的getters则是通过Vue的computed选项完成,更多的细节都写在了代码的注释里了,在这就不一一列举。 详细的实现代码如下
// pvuex.js
let Vue;
function install(Vue_){
Vue = Vue_;
// 混入一个beforeCreate生命周期函数,在Vue原型里注册$store属性
Vue.mixin({
beforeCreate() {
if(this.$options.store){
Vue.prototype.$store = this.$options.store;
}
},
})
}
class Store {
constructor(options){
// 作为在commit执行中的标识符
this._committing = false
// 保存用户的mutations、actions、getters的配置
this._mutations = options.mutations;
this._actions = options.actions;
this._getters = options.getters;
// 代表是严格模式下,不能直接给state里的状态值赋值,只可通过commit的方式提交修改
this.strict = options.strict;
// 定义配置在Vue里的computed,用Vue的computed特性实现getters机制
let computed = {};
// 暴露给外面使用的getters
this.getters = {};
// 把this保存一份
const store = this;
// 遍历每个_getters里的方法,封装一个高阶函数,目的是往遍历的方法里传递一个state
Object.keys(this._getters).forEach(key => {
const fn = this._getters[key];
computed[key] = function(){
return fn(store.state);
}
// 给store.getters的每个属性做一层访问代理,并封印getters里属性的setter
Object.defineProperty(store.getters, key, {
get(){
return store._vm[key];
},
set(val){
console.error('getters not set');
return;
}
});
});
// 借鸡生蛋,借助new Vue完成数据options.state的响应式
this._vm = new Vue({
data: {
$$state: options.state
},
computed
});
// 如果是严格模式,则定义个用户watch,在每个state里的值改变的时候调用assert方法
if(this.strict){
this._vm.$watch(function () { return this._data.$$state }, (v) => {
assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
}, { deep: true, sync: true });
}
// 绑定commit和dispatch函数中的this指向
this.commit = this.commit.bind(this);
this.dispatch = this.dispatch.bind(this);
}
// 通过存取器获取响应后的state
get state(){
return this._vm._data.$$state;
}
// 直接覆盖state,将抛出错误
set state(val){
throw new Error();
}
commit(type, payload){
const entry = this._mutations[type];
if(!entry){
console.error('unkown mutation type');
}
this._wrappedCommit(() => entry(this.state,payload))
}
dispatch(type, payload){
const entry = this._actions[type];
if(!entry){
console.error('unkown action type');
}
entry(this,payload);
}
// 封装commit执行
_wrappedCommit (fn) {
const committing = this._committing;
this._committing = true;
fn();
this._committing = committing;
}
}
// 不合法抛出错误
function assert (condition, msg) {
if (!condition) throw new Error(`[vuex] ${msg}`);
}
export default { Store , install}
测试
测试方案:新建vuecli项目作为测试项目,新建自己的pstore文件夹,把src/pstore/index.js里之前引入的vuex替换成我们自己实现的pvuex.js,在src/main.js里使用src/pstore/index.js导出的store,其他都不变,运行项目看看是否能正常运行。具体代码更改如下:
src/pstore/index.js
src/main.js
src/HelloWorld.vue
页面初始化展示。
点击提交commit、dispatch后页面更新后,页面也显示正常。
至此,测试项目确实保持正常运行,证明我们实现的简版Vuex没什么大问题。希望看完此文,大家能对Vuex3有更深的理解,写的若有失偏颇,尽可在评论里大家一起讨论。今天拜拜。