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。

先介绍下前置知识:

  1. 泛型的反向推导。
  2. 索引签名
  3. 条件类型
  4. keyof
  5. 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']


阅读剩余
THE END