基于Vue3做一套适合自己的状态管理(三)继承:Option 风格的状态

472 阅读4分钟

计划章节

  1. 基类:实现辅助功能
  2. 继承:充血实体类
  3. 继承:OptionApi 风格的状态
  4. Model:正确的打开方式
  5. 组合:setup 风格,更灵活
  6. 注册状态的方法、以及局部状态和全局状态
  7. 实践:当前登录用户的状态
  8. 实践:列表页面需要的状态

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 都可以。

看看结构

405option结构.jpg

依然是“三层结构”,属性、action和getter、以及辅助函数。我喜欢这种结构清晰的设置方式,如果都是挤在第一层的话,总感觉乱糟糟的。

源码

gitee.com/naturefw-co…

在线演示

naturefw-code.gitee.io/nf-rollup-s…