基于Vue3做一套适合自己的状态管理(三)继承:Option 风格的状态
计划章节
- 基类:实现辅助功能
- 继承:充血实体类
- 继承:OptionApi 风格的状态
- 组合:setup 风格,更灵活
- Model:正确的打开方式
- 注册状态的方法、以及局部状态和全局状态
- 列表页面需要的状态
- 当前登录用户的状态
optionAPI 风格的状态
大家可能会发现一个小问题,上一章的方法写起来有点费劲,因为js的风格是灵活随意,而这种方式看着有点别扭。
我们可以参考 optionAPI 的风格封装一下。
创建一个状态
为了便于理解,我们先看看创建状态的方式:
// 定义一个类型
export type ITest = {
name: string;
age: number;
get getName(): string;
get getAge(): number;
addAge(n?: number): void;
addAge2(n?: number): void;
}
const state = OptionState<ITest>('test_OptionState', {
state: () => {
return {
name: '基础设置',
age: 20
}
},
getters: {
getName() {
return this.name + '普通函数'
},
getAge: (state: any) => {
return state.age + '箭头函数'
}
},
actions: {
addAge(n = 1) {
this.age += n
},
addAge2: (state: any, n = 1 ) => {
state.age += 10 * n
}
},
options: {
isLog: true
}
})
不太会 TS 的类型推导,所以需要显性定义一个类型。
和 Pinia 有些相似,设置state、getters 和action。但只是相似,并不完全一样。
封装方法
再来看实现内部的封装方式:
/**
* 传入参数,创建有getter、actions 的状态,reactive
* @param id 状态的标志
* @param info StateCreateOption state、getter、action、options
* * state:状态:对象、数组,或者函数
* * getters?:变成 computed 的对象集合
* * actions?: 变成 action 的对象集合
* * options?: 选项
* * * isLocal —— true:局部状态;false:全局状态(默认属性);
* * * isLog —— true:做记录;false:不用做记录(默认属性);
*/
export default function optionState<T>(id: IStateKey, info: IStateCreateOption): T & IState {
// 判断 state 是 object 还是 array,继承不同的基类
let tmp = null
let basec: any = null
// 根据 options 判断,是否需要做日志
const isLog = !!info.options?.isLog
const _state = (typeof info.state === 'function') ? info.state(): info.state
if (Array.isArray(_state)) {
// 数组,定义子类,在子类上面加 getter 和 action
class arrayClass extends BaseArray {
constructor(_info: any) {
super(_info, id) // 调用父类的constructor()
}
}
basec = arrayClass
} else {
// 对象,定义子类,在子类上面加 getter 和 action
class objClass extends BaseObject {
constructor(_info: any) {
super(_info, id, isLog) // 调用父类的constructor()
}
}
basec = objClass
}
// 创建实例
tmp = new basec(info.state)
// 套上 reactive
const ret = reactive(tmp)
// 挂载 getters,变成 computed
if (typeof info.getters === 'object') {
Object.keys(info.getters).forEach(key => {
// 在子类的原型上面挂载 computed
basec.prototype[key] = computed(() => {
const re = (info.getters as IAnyObject)[key].call(ret, ret)
return re
})
})
}
// 挂载 actions
if (typeof info.actions === 'object') {
Object.keys(info.actions).forEach(key => {
// 在子类的原型上面挂载 action
basec.prototype[key] = async function (...arg: Array<any>) {
writeLog(ret as IAnyObject, {}, `action-${key}`, 3, async () => {
const fun = (info.actions as IAnyObject)[key]
if (fun.toString().match(/^\(.*\) => \{/)) {
// 箭头函数
await (info.actions as IAnyObject)[key].call(ret, ret, ...arg)
} else {
// 普通函数
await (info.actions as IAnyObject)[key].call(ret, ...arg)
}
})
}
})
}
return ret as T & IState
}
初始化
首先需要判断一下状态是对象还是数组,虽然一般状态都是对象,但是也不能不让传数组进来。
如果是对象,则继承 BaseObject;如果是数组则继承 BaseArray,然后创建一个实例,套上 reactive。 最后向子类的原型上面挂载getter(computed)和action。
相当于在运行时创建了一个子类。。。
注意:需要先生成实例套上 reactive 之后,才能在原型上挂载getter、action,否则没有响应性。
挂载 getter
根据 参数里的 getters ,创建 computed 挂载在子类原型上面,然后使用 call 调用函数,因为需要改变 this 的指向。
为什么要传两次参数?因为要兼容普通函数和监听函数,普通函数,第一个参数是 this,第二个可以被忽略;而对于箭头函数,因为没有this,所以第一个参数“无效”,第二个参数才会被接收。
挂载 action
和挂载 getter 基本一样,这里需要判断一下是普通函数还是箭头函数,因为call的参数不一致。
action 需要传递的参数可能不止一个,所以需要分别对待。那么如何判断一个函数是不是箭头函数呢?找了一些资料,居然没有什么“正规”的方法,似乎只能用判断 toString() 的开头部分是不是(xxx) => {
的方法。
- 为什么要用 await? 因为action 可能是异步的,需要等待回调完毕,才能获得变更后的状态,这样才好做日志。也就是说,action 可以支持异步,但是需要使用 await 的异步方式,否则影响日志的新值的记录。
当然如果你不需要做日志的话,那么用不用 await 都可以。
看看结构
依然是“三层结构”,属性、action和getter、以及辅助函数。我喜欢这种结构清晰的设置方式,如果都是挤在第一层的话,总感觉乱糟糟的。
源码
在线演示
转载自:https://juejin.cn/post/7236988255073681463