基于Vue3做一套适合自己的状态管理(二)继承:充血实体类
计划章节
- 基类:实现辅助功能
 - 继承:充血实体类
 - 继承:Option 风格的状态
 - Model:正确的打开方式
 - 组合:setup 风格,更灵活
 - 注册状态的方式和状态的有效范围
 - 列表页面需要的状态
 - 当前登录用户的状态
 
继承:使用充血实体类的方式实现状态
上一篇实现了一个基类(BaseObject),实现了几个辅助功能($state、$patch等),基类在初始化的时候,依据传入的参数创建属性。实现辅助功能之后,继续发展出现了分支:
- 继承:充血实体类的风格,一层一层往下继承(派生)。
 - 组合:compositionAPI的风格,组合代替继承。
 
结合实际需求,我觉得两个都可以有,继承适合简单的状态,组合适合复杂的需求。
继承出来一个新状态
我们如果要实现一个 person 的状态,可以继承 BaseObject 定义一个子类:
  class Person extends BaseObject {
    name: string
    age: number
    constructor() {
      super({})
      // 设置属性
      this.name = '子类的名字'
      this.age = 18
    }
    // 设置方法
    add() {
      this.age += 1
    }
  }
继承 BaseObject 可以得到辅助功能,然后设置自己的属性和方法。
然后就是实现响应性的问题了,可以有两种方法:
- 使用 ref:像 Pinia 那样,每一个属性都是 ref 的形式;
 - 使用 reactive:class 的实例套上 reactive。
 
使用 ref 实现响应性
我们可以在初始化的时候使用ref,当然也可以使用 computed:
  import { reactive, computed, toRaw, ref } from 'vue'
  import type { ComputedRef, Ref } from 'vue'
  
  class Person extends BaseObject {
    name: Ref<string>
    age: Ref<number>
    testComputed: ComputedRef
    constructor() {
      super({})
      // 设置属性,直接使用 ref
      this.name = ref('ref')
      this.age = ref(20)
      // 直接使用 computed
      this.testComputed = computed(() => {
        return this.name.value + '测试 computed'
      })
    }
 
    // 设置方法,action
    add() {
      this.age.value += 1
    }
  }
这样我们就实现了响应性。其实,同理,属性也可以使用 reactive、readonly
看看结构

层次非常分明,第一层是属性和getter,第二层是action,第三层是辅助函数。 这里的 getter 是一个类型是computed 的属性。
使用 reactive 实现响应性
Pinia会把每一个属性都变成 ref 的形式,我个人不太喜欢,感觉有点浪费,为啥不直接使用reactive?
在初始化的时候,属性使用原生的 js 类型,然后在 new 的时候套上 reactive ,感觉这样效率可以更高一些:
  class Person2 extends BaseObject {
    #__getters: {[key: string]: ComputedRef}
    name: string
    age: number
    constructor() {
      super({})
      this.#cp = {}
      // 设置属性
      this.name = '子类的名字'
      this.age = 18
    }
    // 设置 computed
    get getName() {
      const re = toRaw(this).#__getters['getName']
      if (re){
        return re
      } else {
        return toRaw(this).#__getters['getName'] = computed(() => {
          return this.name + '--测试 computed'
        })
      }
    }
    // 设置方法
    add() {
      this.age += 1
    }
  }
  
  // 创建实例
  const person = new Person()
  // 套上 reactive
  const nfState  = reactive(person)
ref 和 reactive 各有优势,大家喜欢哪种方式呢?
神奇的 this 指向
看了上面 reactive 的方式,可能你会觉得奇怪,这样写(add、getName)会有响应性吗?——会的,因为this的指向是一件很神奇的事情。
初始化的时候 this 指向的是普通的对象(class),这时候没有响应性,所以初始化的时候不能直接设置computed,除非属性直接使用 ref,或者reactive,但是这样就变成第一种方式了。
但是创建的实例,套上 reactive 之后, this 就会指向 reactive (Proxy)。这样在 add 里面使用 this 就相当于操作 reactive,所以有响应性;同理 computed 也是一样。
缓存 computed
这里的computed有点小问题 —— 每次访问都会生成一个新的 computed,所以还要进行一下修改,可以加上缓存。
    get getName() {
      const re = toRaw(this).#cp['getName']
      if (re){
        return re
      } else {
        return toRaw(this).#cp['getName'] = computed(() => {
          return this.name + '测试 computed'
        })
      }
    }
这样调整之后,不会每次都创建一个新的 computed 了,只是每次还是需要判断一下。
看看结构

结构稍有不同,第一层是属性,getter和action在第二层,辅助函数依然在第三层。 这里的 getter 是一个函数,返回了一个computed。
源码
在线演示
转载自:https://juejin.cn/post/7236940669066330171