服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

Vue3使用class + reactive打造一套超轻状态管理

日期: 来源:脚本之家收集编辑:自然框架
将 脚本之家 设为“星标
第一时间收到文章更新

作者 | 自然框架
出品 | 脚本之家(ID:jb51net)

Pinia 的状态管理非常优秀,只是我喜欢“充血实体类”风格的状态管理,于是使用 ES6 的 class 设计了一个适合自己需求的状态模式。

设计一个简单的状态。

我们先用 ES6 的 class 设计一个简单的状态,比如当前访问用户。(采用 TypeScript 的方式)

/**
* 登录用户的状态
*/
export default class UserState {
// 可以增加其他属性
name: string
department: number | string
rules: {
modules: Array<number | string>
}
// 初始化,设置默认属性值
constructor() {
this.name = '没有登录'
this.department = 0
this.rules = {
modules: []
}
}

// 操作状态的函数
/**
* 登录,设置状态
*/
async login() {
// 实现登录的代码,这里仅模拟
this.rules.modules.push(100)
this.name = '登录成功!'
}

/**
* 退出登录,更改状态
*/
async signOut() {
// 实现退出的代码,仅示例
this.rules.modules.length = 0
this.name = '没有登录'
}

/**
* 验证权限
*/
verifyPermissions(modulesId: number | string) {
return this.rules.modules.includes(modulesId)
}
// 可以设置其他操作的函数

}

状态的结构

我们来获取一个实例,然后打印出来看看内部结构:

department: 0
name: "登录成功!"
rules: {modules: Array(1)}
[[Prototype]]: Object
constructor: class UserState2
login: ƒ login()
signOut: ƒ signOut()
verifyPermissions: ƒ verifyPermissions(modulesId)

属性和函数分为两个层次,第一层的属性用来表示状态,第二层的函数用来获取状态或者变更状态,属性和函数都可以灵活扩展。

优点

  • 状态和变更方式分离
    状态在“第一层”,操作方式在“第二层”,看着不乱,可以直接使用原生 js 的方法,比如 Object.keys、Object.assign 等。

  • 直接遍历状态
    使用Object.keys 获取key 的时候,只有属性部分(不含私有成员),没有函数部分,方便遍历等操作,无需自己写辅助函数处理。

  • 直接使用 Vue 提供的各种API
    可以直接使用 reactive、readonly、toRefs等,函数不会出来“捣乱”。

状态的响应性问题

class 本身是没有响应性的,不过不用担心,Vue3 的 composition API 提供了多种响应方式,比如 reactive、ref、computed 等。

reactive

使用 reactive 是很简单的,我们只需要把 class 的实例放入 reactive 即可实现响应性,因为 “实例”本身也是对象。

  // 创建实例
const user = new UserState()
console.log('user', user)
// 实现响应性
const userState = reactive(user)
console.log('personState', userState)

看看打印效果(userState —— Proxy):

[[Handler]]: Object
[[Target]]: UserState
department: 0
name: "没有登录"
rules: {modules: Array(0)}
[[Prototype]]: Object
constructor: class UserState2
login: ƒ login()
signOut: ƒ signOut()
verifyPermissions: ƒ verifyPermissions(modulesId)
[[Prototype]]: Object

ref、computed

我们可以在初始化的时候,把属性设置为 ref 或者 computed 的形式,当然如果有需要,我们也可以使用 reactive、readonly 等。

get 访问器也可以 return computed,只不过每次调用的时候,都会返回一个 computed。

实现只读状态

上面的代码虽然可以实现基础的状态功能,但是有个小问题:状态的属性可以随意变更!

我们来设想一下现实中的需求:

  • 宽松模式:简单需求可以直接变更状态;
  • 严格模式:不能随意变更状态。

那么能否限制状态的随意变更呢?当然可以,我们可以使用 class 的私有成员,或者使用 vue3 的 readonly 来实现。

使用私有成员禁止随意变更状态

ES6 的 class 提供了私有标记 #(ES2022),我们可以把不希望随意变更的状态(属性)设置为私有成员,这样可以禁止外部直接变更状态,安全感满满。

/**
* 私有成员的状态
*/
export class UserState {
#name: Ref<string> // 有响应性
#department: number | string // 没有响应性,外面加上 reactive 也不行
#rules: {
modules: Array<number | string>
}

constructor () {
this.#name = ref('测试私有成员的响应性')
this.#department = 18
this.#rules = {
modules: []
}
}

/**
* 获取name
*/
get name(): string {
// 需要返回 value,否则 name 可以被修改
return toRaw(this).#name.value
}

/**
* 获取部门,验证 原生对象 无法实现响应性
*/
get department() {
// 取原型,失去响应性;不取原型,this 指向被改变,无法访问私有成员。
return toRaw(this).#department
}

// 操作状态的函数
// 同上 略,
}
  • 用 # 标记私有成员
    在属性前面加上 # 号,这样使用的时候就无法直接改变值,必须使用特定函数才能变更。

  • 使用 get 访问器获取私有成员
    想要获取私有成员的话,需要设置 get 访问器 。

  • 为什么使用 toRaw?因为实现响应性的时候会套上 reactive,而 reactive 的本质是 Proxy,Proxy 在拦截 get 操作后,会改变 this 指向,导致 class 内部的 this 无法获取私有成员,所以需要使用 toRaw 获取原来的 this。

  • 私有成员为什么要使用 ref?解决私有成员的响应性的一种方法。

私有成员与 reactive 的冲突

class 的实例加上 reactive 即可实现响应性,但是如果其中有私有成员就会出现问题。
reactive 的本质是 Proxy,拦截 get 后会改变 this 指向,导致无法访问私有成员。
如果使用 toRaw(this) 取原型,那么会导致失去响应性。
目前的解决方法是使用 ref 来实现私有成员的响应性,但是这样的结构整体看起来就有点臃肿。(不太喜欢Pinia的状态的属性都是 ObjectRefImpl。)

私有成员的结构

在打印的时候可以看到私有成员,但是在 Object.keys () 、for in 里不会出现私有成员和 get 方法。
在 js 代码里可以通过 get 方式获取状态。

  #department: 18
#name: RefImpl
#rules: Object
name: (...)
department: (...)
[[Prototype]]: Object
name: (...)
constructor: class UserState
department: (...)
login: ƒ async login()
signOut: ƒ async signOut()
verifyPermissions: ƒ verifyPermissions(modulesId)
get name: ƒ name()
get department: ƒ department()
[[Prototype]]: Object

使用 readonly 禁止随意变更状态

如果担心浏览器不支持 class 的私有成员的话,可以使用 Vue3 的 readonly 来实现只读的状态。

我们把状态放入 readonly 即可。

  const user = new UserState()
const userState = reactive(user)
const userRead = readonly(userState)

对比

  • 私有成员

    • 可以精确设置某个状态只读,但是只读属性不会出现在Object.keys () 里 ;
    • 和 reactive 有点小冲突,需要使用 ref 实现响应性。
  • readonly

    • 只能把全部状态(属性)都设为只读。
    • 可以阻止随意增减状态的属性。

使用 js 的风格封装一下

后端的语言习惯使用 class,使用的时候 new 一下很自然,但是在 js 里面可能不太习惯使用 new,那么我们可以再封装一下。

  // 可以写个函数封装一下
function mystate () {
const re = new UserState()
return reactive(re)
}

ref、computed 等就是使用 class 实现的,然后又封装了一下 new 。

共享问题

状态做好了,那么如何让其他组件共享呢?我们可以使用全局变量,或者使用 provide / inject 。

全局变量

如果不需要考虑 SSR 的话,使用全局变量是一种很简单的方式。我们可以建立一个 js 文件(或者 ts 文件),然后导出一个全局变量即可。

我们写一段简单的代码测试一下这个想法:

// 在main、组件里面引入这个文件。
/**
* 单例模式创建状态
*/
// 容器
export const mystore:{[key: string]: any} = {}

// 模拟状态类
class Foo {
name: string
constructor () {
this.name = '测试静态状态'
}
}

// 初始化
const run = () => {
const foo = new Foo ()
mystore['s'] = foo // 存入容器
}

// 判断是否已经初始化
if ( Object.keys(mystore).length === 0 ) {
run()
}

上面只是一个简单的例子,验证一下想法是否确实可行。
测试结果和预想的一样,这种方式可以看做是一个单列模式。

注入

Vue 提供 provide / inject 实现注入,这是一种很方便的共享方式,既可以实现全局状态,也可以实现局部状态。

  • 全局状态,我们可以在 main 里面注入;
  • 局部状态,我们需要在对应的父组件里面注入。

设计几个辅助功能

Pinia 提供了 patch 等几个内置函数,解决了 reactive 赋值麻烦的问题,那么我们是否可以借鉴一下呢?

如果要加上内置函数,那么使用组合还是使用继承?Vue3 采用的是组合,其实对于简单的需求,我们也可以尝试一下继承的方式。

我们可以设计一个基类,在基类里面统一实现内置函数,然后定义其他状态时继承这个基类即可。

实现 $reset

看到这个功能时想到了表单的重置功能,Pinia 实现这个功能,不会就是为表单而做的吧。(话说表单的 model 属于状态吗?)

好了不纠结这个问题,我们来看看如何实现。

想要实现重置,首先需要知道初始值,初始值有两种情况,一个是对象,一个是函数。

如果是函数的话比较方便,再调用一次函数就可以得到初始值。如果是对象的话,由于“引用”问题,所以对象的“初始值”会发生变化。

可能是因为想要简化操作,Pinia 规定 state 需要使用函数的方式。

那么如果 state 是对象的方式呢?其实我们只需要在初始化的时候深拷贝留一个备份即可。不过说起来简单实现起来麻烦,js 的深拷贝可不是随便写写就行的。

所以我们也可以参照 Pinia 的设计综合一下,单层属性的状态可以直接使用对象,多层的或者复杂的状态,需要使用函数的方式。

/**
* 使用基类实现状态的共用函数
*/
export class NFState {
#_value: IObjectOrFunction

constructor (obj: IObjectOrFunction) {
if (typeof obj === 'function'){
// 记录初始函数
this.#_value = obj
// 执行函数,浅层拷贝,设置属性
Object.assign(this, obj())
}
else {
// 记录初始值的副本,浅层拷贝,只支持单层属性
this.#_value = Object.assign(obj)
// 浅层拷贝,设置属性
Object.assign(this, obj)
}
}

// 操作状态的函数
/**
* 获取初始值,如果是函数的话,会调用函数返回结果
*/
get $value() {
const val = toRaw(this).#_value
const re = typeof val === 'function' ? val() : val
return re
}

/**
* 恢复初始值
*/
$reset() {
// 模板里面触发的事件,没有 this
if (this) {
copy(toRaw(this), this.$value)
}
}
}

在初始化的时候保存初始值,然后设置一个访问器获取初始值,最后在 state 赋值即可。

实现 $state

实现这个功能的时候纠结了好久,按照 js 的风格,对象定义之后可以增减属性,但是按照 ts 风格和状态的设定,对象(状态)定义之后不应该增减属性。

于是还是先看看 Pinia 的 $state 的实现方式。

Pinia 的 state 测试结果:

  • 状态定义之后不能通过 $state 增减属性。
  • 按照 ts 规则,必须使用完整的属性才可以赋值,如果不完整会出现提示(Typescript)。但是可以“强行”运行,运行时可以修改部分属性值(效果等同 $patch)。

所以我们做一个简单的 $state。

  • 以定义的状态的属性为准,不能增加属性,也不需要减少属性。
  • 不支持私有成员,因为私有成员遍历不出来,另外私有成员也不能随意变更状态。
  • 不支持 readonly,set 访问器会被拦截。
  • 只支持“两层”的拷贝,更深的直接覆盖地址。我觉得复杂的状态应该拆分成多个小的状态。
  • 取原型遍历,set 访问器会触发响应。
  /**
* 设置新值
*/
set $state(value: IAnyObject) {
// 要不要判断 value 的属性是否完整?
copy(toRaw(this), value)
}

这里并没有判断 value 的属性是否和定义的状态是否一致,因为 Pinia 似乎也没有判断(运行时),我们也就先不实现了。

实现 $patch

patch就是函数的形式,这是为了直观感觉吗?
测试了一下 Pinia 的 state 基本一样。好吧,官网说,patch。

$patch 的参数可以是函数也可以是一个对象,如果是函数的话,则把状态作为参数调用函数;如果是对象的话吗,那么调用内部的 copy 函数。

Pinia 提供 $patch 的目的,应该是方便实现“时间线”,那么我们是否要实现呢?暂时先不实现了因为还没弄懂怎么和 devTool 通讯。

  /**
* 替换部分属性,只支持单层
*/
async $patch(obj: IObjectOrFunction) {
if (typeof obj === 'function') {
// 回调,不接收返回值
await obj(this)
} else {
// 赋值
copy(this, obj)
}
}

实现拷贝的函数

好了我们来看看拷贝函数要如何来实现。常见的情况是浅层拷贝和深层拷贝,但是 Vue 提供了响应性,而对象的直接赋值会破坏这个响应性,所以我们还要考虑 reactive 和 ref 的情况。

所以我做了这个“半深考”的拷贝方法。

/**
* 以 target 的属性为准,进行赋值。支持部分深层copy
* * 如果属性是数组的话,可以保持响应性,但是不支持深层copy
* * 如果 有 $state,会调用。
* @param target 目标
* @param source 源
*/
function copy(target: IAnyObject, source: IAnyObject) {
const _this = target
const _source = toRaw(source)

// 以 原定状态的属性为准遍历,不增加、减少属性
for (const key in _this) {
const _val = unref(_source[key]) // 应对 ref 取值
const _target = _this[key]

if (_val) { // 如果有值
if (_target.$state) { // 对象、数组可以有 $state。
_target.$state = _val
} else {
if (Array.isArray(_target)) { // 数组的话,需要保持响应性
_target.length = 0
if (Array.isArray(_val)) // 来源是数组,拆开push
_target.push(..._val)
else
_target.push(_val) // 不是数组直接push

} else if (typeof _target === 'object') { // 对象,浅拷
Object.assign(_this[key], _val)
} else {
if (isRef(_this[key])) { // 还得考虑 ref
_this[key].value = _val
} else {
_this[key] = _val // 其他,赋值
}
}
}

}
}
}

实现子类

基类设计好之后,我们只需要继承一下即可:(仅示例)

class Foo extends NFState {
sonName: string

constructor(obj: IObjectOrFunction) {
super(obj) // 调用父类的constructor()
this.sonName = '子类的属性' // 设置之类属性
}
}
const a = new Foo({})

源码

https://gitee.com/naturefw-code/nf-rollup-state

本文作者:自然框架

个人网址:jyk.cnblogs.com

声明:本文为 脚本之家专栏作者 投稿,未经允许请勿转载。


写的不错?赞赏一下


长按扫码赞赏我

    推荐阅读:原创推荐:

【人人都可低代码】Vue3 把 el-form 变成LowCode风格的表单控件

结合 Vuex 和 Pinia 做一个适合自己的状态管理nf-state

一篇文章说清 webpack、vite、vue-cli、create-vue 的区别

【摸鱼神器】vue + 路由 + 菜单 + tabs 一次搞定之管理后台

【摸鱼神器】UI库秒变LowCode工具——列表篇(一)

20多个好用的 Vue 组件库,请查收!

基于Vue3 写一个更好用的在线帮助文档工具

通过UI库深入了解Vue的插槽的使用技巧(片尾有彩蛋)

【摸鱼神器】拖拖点点,列表现——做列表居然可以不用写代码!

相关阅读

  • 165K Star!面试有这个项目,稳了!

  • 关注“脚本之家”,与百万开发者在一起原创:开源小分队(微信公众号ID:sourceteam)已获得原公众号授权转载今天了不起给大家推荐一个非常牛的JavaScript算法与数据结构项目-javasc
  • ChatGpt实现的短消息发送Redis限流功能

  • 「 关注“石杉的架构笔记”,大厂架构经验倾囊相授 」文章来源:【公众号:业余草】前言ChatGpt实现的短信发送Redis限流功能本文的内容和源码都来自于 ChatGpt,大家感受一下 ChatG
  • 7min 到 40s:SpringBoot 启动优化实践!

  • 关注我,回复关键字“spring”,免费领取Spring学习资料。来源:https://juejin.cn/post/71813425237285929550 背景公司 SpringBoot 项目在日常开发过程中发现服务启动过程异常缓
  • Spring Boot中一个注解优雅实现重试

  • 关注我,回复关键字“spring”,免费领取Spring学习资料。重试,在项目需求中是非常常见的,例如遇到网络波动等,要求某个接口或者是方法可以最多/最少调用几次;实现重试机制,非得用Ret
  • Spring Boot 项目鉴权的 4 种方式

  • 关注我,回复关键字“spring”,免费领取Spring学习资料。文章介绍了spring-boot中实现通用auth的四种方式,包括 传统AOP、拦截器、参数解析器和过滤器,并提供了对应的实例代码,最
  • Spring Boot配置保存日志文件

  • 关注我,回复关键字“spring”,免费领取Spring学习资料。springboot日志配置:springboot默认日志是打印再console中的,不会保存在文件中。我们项目上线肯定要保存日志用于分析的
  • 又有城市设立“人才日”,还要思考什么?

  • 文|朱晓帆继宁波、南通、温州之后,又一座城市以法定形式为人才设立了专属的节日。 湖州市景观 新华网发 4月24日,浙江湖州市人大常委会会议表决通过《关于设立“湖州人才日”
  • 有调整!江苏儿童可免费接种11种疫苗

  • 疫苗被公认为是预防传染性疾病的极其重要、有效的工具家长们注意今年江苏儿童疫苗接种有调整可免费接种11种疫苗(点击查看大图)水痘疫苗免费2018年11月起,南京市将水痘减毒活疫

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四
  • 美国对华2000亿关税清单,到底影响有多大?

  • 1 今天A股大跌,上证最大跌幅超过2%。直接导火索是美国证实计划对华2000亿美元产品加征25%关税。 听起来,2000亿美元数目巨大,我们来算笔账。 2000亿美元,按现在人民币汇率

最新文章

  • Vue3使用class + reactive打造一套超轻状态管理

  • 将 脚本之家 设为“星标⭐”第一时间收到文章更新作者 | 自然框架出品 | 脚本之家(ID:jb51net)Pinia 的状态管理非常优秀,只是我喜欢“充血实体类”风格的状态管理,于是使用 ES6
  • 165K Star!面试有这个项目,稳了!

  • 关注“脚本之家”,与百万开发者在一起原创:开源小分队(微信公众号ID:sourceteam)已获得原公众号授权转载今天了不起给大家推荐一个非常牛的JavaScript算法与数据结构项目-javasc
  • 30年不评职称的教师,获评副教授

  • 30年不评职称的华中科技大学教师杨汉文,已经获评副教授。4月24日,澎湃新闻记者查询发现,华中科技大学航空航天学院官网“副教授”一栏更新信息显示,杨汉文职称已标注为副教授。
  • 为什么进程地址空间中包括操作系统?

  • 将 脚本之家 设为“星标⭐”第一时间收到文章更新出品 | 码农的荒岛求生 (ID:escape-it)已获得原公众号的授权转载大家好,今天聊聊进程地址空间这点小事。说到进程的地址空间,大