深入vue3为啥不推荐reactive使用泛型

930 阅读4分钟

最近重新翻刷了官方文档,有那么一句话,不推荐在 reactive ​使用泛型。

image-20221110103609-q8r9k4u.png

然后我就疑惑了,日常使用中,对于 ref ​我们需要限定其中类型时,我们就会通过泛型来约束,而 reactive ​也很自然沿用了这种思维,也没遇到什么问题。

那这里说的不推荐,是什么情况呢?

可能带来的问题

既然这句话写着深层次 ref ​相关,那就尝试写一个例子看看会出现什么问题。

‍ 有这么一段伪代码:我们已有 name ​变量,有一个表单 form​,其中属性 name ​是来自 name ​变量。

interface Form {
  id: string
  name: string
}

const name = '我的名字'

const form = reactive<Form>({
  id: '',
  name
})

这样使用 name ​是没问题的,但是如果 name ​是一个 Ref ​类型的话

const name = ref('我的名字')

const form = reactive<Form>({
  id: '',
  name
})

name 就会报错:Type 'Ref<string>' is not assignable to type 'string'​。

但这时候你可能会想:将 Form ​中的 name ​改为 Ref ​不就好了吗?

interface Form {
  id: string
  name: Ref<string>
}

这时候确实解决报错的问题了,但你会发现另外一个问题,form ​的自动推导类型 name ​是 string​,而我们定义的接口 Form ​里面的 name ​却是 Ref<string>​。

image-20221110144822-mm9wc8l.png

所以问题找到了: reactive带有深层次的 ref时,我们如果通过泛型来约束类型,类型是会对应不上的!

分析

既然问题是出在类型上,我们就深入 reactive ​这个函数的类型看看做了什么事情(vscode 按键 Command​+ 鼠标左键)?

// 这里把大部分相关类型贴出来,一些用不到的就不一一贴出来了

declare type BaseTypes = string | number | boolean;

export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>;

export declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>;

declare type UnwrapRefSimple<T> = T extends Function | CollectionTypes | BaseTypes | Ref | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] | {
    [RawSymbol]?: true;
} ? T : T extends Array<any> ? {
    [K in keyof T]: UnwrapRefSimple<T[K]>;
} : T extends object & {
    [ShallowReactiveMarker]?: never;
} ? {
    [P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>;
} : T;

export declare type UnwrapRef<T> = T extends ShallowRef<infer V> ? V : T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>;

我们通过代入下面的类型 Form​,来分析 reactive<Form> ​会返回什么?

interface Form {
  id: string
  name: Ref<string>
}
  1. reactive ​类型判断一下泛型是否对象,调用 UnwrapNestedRefs<Form>

  2. UnwrapNestedRefs ​类型判断 Form ​是否 Ref ​类型,不是直接调用 UnwrapRefSimple<Form>

  3. UnwrapRefSimple<Form> ​类型里面的类型约束比较多,我们一个个看:

  • 第一个看是否在 Function | CollectionTypes | BaseTypes | Ref | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] | { [RawSymbol]?: true; } ​联合类型其中之一,结果否到下一个类型约束

  • 第二个约束是否 Array<any>​,结果否到下一个类型约束

  • 第三个约束是否是 object & { [ShallowReactiveMarker]?: never; } ​这个交叉类型,Form ​是满足 object ​类型的,而 [ShallowReactiveMarker] ​是一个可选属性,所以满足

  • 满足调用的是 { [P in keyof Form]: P extends symbol ? Form[P] : UnwrapRef<Form[P]>; }​,in keyof ​的意思遍历 Form ​所有的键,每次的键定义为 P ​然后判断是否 symbol

    • 第一个键是 id​,类型时 string​,判断不是 symbol ​所以调用 UnwrapRef<string>​;string ​不满足 ShallowRef<infer V>​,也不满足 Ref<infer V>​,那就调用 UnwrapRefSimple<string>
    • 你会发现又回到第三步了,没错这个就是类型的递归了!这次比较简单发现 string ​是 BaseTypes​,所以直接返回 string​,那么 id ​的类型就还是 string​(饶了一圈结果是它本来的类型,因为这些类型不需要二次处理
    • 第二键是 name​,类型时 Ref<string>​,判断不是 symbol ​所以调用 UnwrapRef<Ref<string>>​;Ref<string> ​不满足 ShallowRef<infer V>​,但满足 Ref<infer V>​,infer ​可以推断出传入类型的泛型 V​,那就是 string ​了,结果调用 UnwrapRefSimple<string>​(注意这里类型变了,这个就是解包的过程);然后又调用 UnwrapRefSimple<string>​,最后结果就是 name 的类型就变成 string
  1. 最后我们得出返回就是:
{
  id: string
  name: string
}

上面的类型源码用了很多高级用法,这里列举一下相关官方出处:

  • 联合类型 | :返回一个新类型,表达多个类型其中之一,配合 extends 意思是否在这么多个类型之一
  • 交叉类型 & :通常用于两个接口的合并为一个新接口,配合 extends 可以理解为要完全满足这个新的接口
  • 条件类型:类型的条件判断,利用这个可以写出判断嵌套、递归等高级用法
  • infer:在条件类型中可以推断出传入类型的泛型
  • 映射类型:用来遍历类型

结论

很显然,reactive ​函数会做一个隐式的解包操作,如果通过泛型来约束类型,可能在某些场合下带来一些类型的疑惑,所以不推荐使用。那么我们在日常的使用下,需要类型约束直接显示定义即可:

const form: Form = reactive({
  id: '',
  name
})

但如果你真的需要使用泛型,建议去细细品味源码类型一下做了什么,能够更好地规避类似的问题。