TS | UnwrapRef 的底层解包原理
Vue3 中,ref
是一个新出现的 api,ref
这个函数,它会为简单类型的值生成一个形为 { value: T }
的包装。
ref
函数所返回的类型 Ref
,就是本文要讲解的重点了。
假如 ref
函数中又接受了一个 Ref
类型的参数呢?Vue3 内部其实是会帮我们层层解包,只剩下最里层的那个 Ref
类型,最后只会剩下 { value: number }
这个类型。
const count = ref(ref(ref(ref(2))))
这是一个好几层的嵌套,按理来说应该是 count.value.value.value.value
才会是 number
,但count.value指向的就是number。
先介绍下前置知识:
- 泛型的反向推导。
- 索引签名
- 条件类型
- keyof
- infer
泛型的反向推导
function create<T>(val: T): T
let num: number
const c= create(num)
泛型没传入也可以推断出value为number
索引签名
type Test = {
foo: number;
bar: string
}
type N = Test['foo'] // number
条件类型
type TypeName<T> = T extends string
? "string"
: T extends boolean
? "boolean"
: "object";
type T0 = TypeName<string>; // "string"
type T1 = TypeName<"a">; // "string" "a"是string的子类
type T2 = TypeName<true>; // "boolean"
用 extends
关键字配合三元运算符来判断传入的泛型是否可分配给 extends
后面的类型。
keyof
keyof
操作符是 TS 中用来获取对象的 key 值集合的
type Obj = {
foo: number;
bar: string;
}
type Keys = keyof Obj // "foo" | "bar"
infer
文档中对它的描述是 条件类型中的类型推断。可以把infer f 类比成for中的let i ,相当于一个占位符
注意:前置条件:infer要用在条件类型中
type Unpack<T> = T extends Array<infer R> ? R : T
type NumArr = Array<number>
type U = Unpack<NumArr>
// 经过计算
type Unpack<Array<number>> = Array<number> extends Array<infer R> ? R : T
//此处的r 被推断成number
// 得到
number
ref
Ref类型:type Ref<T = any> = { value: T }
// 这里用到了泛型的默认值语法 <T = any>
type Ref<T = any> = {
value: T
}
function ref<T>(value: T): Ref<T>
const count = ref(2)
count.value // number
如果传入给函数的 value 也是一个 Ref
类型呢?比如返回Ref<Ref<number>>
function ref<T>(value: T): T extends Ref //用上一个例子为例,T为Ref<number>
? T
: Ref<UnwrapRef<T>>
//这个函数可以解包,但只可以解第一层
递归 UnwrapRef
type UnwrapRef<T> = {
ref: T extends Ref<infer R> ? R : T //若为多层嵌套 Ref<Ref<Ref<number>>>则走后面的逻辑,
other: T
}[T extends Ref ? 'ref' : 'other']
首先假设我们调用了 ref(ref(2))
我们其实会传给 UnwrapRef
一个泛型:
UnwrapRef<Ref<Ref<number>>>
那么第一次走入 [T extends Ref ? 'ref' : 'other']
这个索引的时候,匹配到的是 ref
这个字符串,然后它去
type UnwrapRef<Ref<Ref<number>>> = {
// 注意这里和 infer R 对应位置的匹配 得到的是 Ref<number>
ref: Ref<Ref<number>> extends Ref<infer R> ? UnwrapRef<R> : T
}['ref']
匹配到了 ref
这个索引,然后通过用 Ref<Ref<number>>
去匹配 Ref<infer R>
拿到 R
也就是解包了一层过后的 Ref<number>
。
再次传给 UnwrapRef<Ref<number>>
,又经过同样的逻辑解包后,这次只剩下 number
类型传递了。
也就是 UnwrapRef<number>
,那么这次就不太一样了,索引签名计算出来是 ['other']
,
也就是
type UnwrapRef<number> = {
other: number
}['other']
自然就解包得到了 number
这个类型,终止了递归。
这里放一下 Vue3 里的源码,在源码中对于数组、对象和计算属性的 ref
也做了相应的处理
export interface Ref<T = any> {
[isRefSymbol]: true
value: T
}
export function ref<T>(value: T): T extends Ref ? T : Ref<UnwrapRef<T>>
export type UnwrapRef<T> = {
cRef: T extends ComputedRef<infer V> ? UnwrapRef<V> : T
ref: T extends Ref<infer V> ? UnwrapRef<V> : T
array: T
object: { [K in keyof T]: UnwrapRef<T[K]> }
}[T extends ComputedRef<any>
? 'cRef'
: T extends Array<any>
? 'array'
: T extends Ref | Function | CollectionTypes | BaseTypes
? 'ref' // bail out on types that shouldn't be unwrapped
: T extends object ? 'object' : 'ref']