最近重新翻刷了官方文档,有那么一句话,不推荐在 reactive
使用泛型。
然后我就疑惑了,日常使用中,对于 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>
。
所以问题找到了: 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>
}
-
reactive
类型判断一下泛型是否对象,调用UnwrapNestedRefs<Form>
-
UnwrapNestedRefs
类型判断Form
是否Ref
类型,不是直接调用UnwrapRefSimple<Form>
-
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
- 第一个键是
- 最后我们得出返回就是:
{
id: string
name: string
}
上面的类型源码用了很多高级用法,这里列举一下相关官方出处:
结论
很显然,reactive
函数会做一个隐式的解包操作,如果通过泛型来约束类型,可能在某些场合下带来一些类型的疑惑,所以不推荐使用。那么我们在日常的使用下,需要类型约束直接显示定义即可:
const form: Form = reactive({
id: '',
name
})
但如果你真的需要使用泛型,建议去细细品味源码类型一下做了什么,能够更好地规避类似的问题。