源码中的TS类型记录
类型谓词is的使用(TS类型收窄方式)
- TS类型收窄方式
类型谓词是“用户自定义类型保护”中唯一且核心的语法形式
IfAny
typescript
export type UnwrapRef<T> = IfAny<T, T,
T extends Ref<infer V>
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>
>
// 如果没有 IfAny 保护:
// UnwrapRef<any> 会进行递归解包,可能导致深层嵌套的类型计算
// 有了 IfAny,直接返回 any,更高效且符合直觉
为了更全面地理解 IfAny 的行为,将其与TypeScript中其他特殊类型进行对比测试:
| 测试类型 | 1 & T | 0 extends 1 & T | IfAny<T, 'Y', 'N'> | 说明 |
|---|---|---|---|---|
any | any | true | 'Y' | 目标检测类型 |
unknown | 1 | false | 'N' | 与 any 不同,unknown 更安全 |
never | never | false | 'N' | 空类型 |
1 | never | false | 'N' | 字面量类型 |
string | never | false | 'N' | 普通类型 |
any[] | any | true | 'Y' | 数组包含 any 元素,整个类型被视为 any |
{ x: any } | any | true | 'Y' | 对象包含 any 属性,整个类型被视为 any |
重要发现:IfAny 不仅检测纯粹的 any,也检测包含 any 的复合类型(如 any[]、{x: any})。这是因为 any 的“传染性”在交叉类型中会传播。
any & T 的结果总是 any,无论 T 是什么类型。这是 any 类型的特殊“传染性”。
unknown作为顶层类型,与其他类型交叉时有特殊的规则,即T & unknown为T
1&never为never,是因为never是空集,任何类型与空集的交集都是空集
| 特性 | any | unknown | ||
|---|---|---|---|---|
| 类型层级 | 特殊的“开关”类型,既是父类型也是子类型 | 顶层类型(所有类型的父类型) | ||
| 交叉行为 | 传染性:T & any → any | 中性:T & unknown → T | ||
| 联合行为 | 传染性:`T | any → any` | 吸收性:`T | unknown → unknown` |
| 类型安全 | ❌ 不安全(绕过所有检查) | ✅ 安全(需要显式检查) |
关键结论:1 & any 结果是 any,不是因为 any 是“所有类型的子集”,而是因为 TypeScript 对 any 的特殊设计——它是一个具有传染性的类型黑洞,与任何类型结合都会吞噬对方成为 any。
为什么[T] extends [Ref]不写成T extends Ref
- 核心区别:是否触发“分布式条件类型”
- 这是 TypeScript 条件类型的一个高级特性。当
extends左侧是裸类型参数时,会触发“分布式条件类型”,否则不会。
typescript
// 情况1:裸类型参数 - 触发分布式
type Test1<T> = T extends Ref ? 'Yes' : 'No'
// 当 T 是联合类型时,会分别检查每个成员
// 情况2:包裹类型参数 - 不触发分布式
type Test2<T> = [T] extends [Ref] ? 'Yes' : 'No'
// 将 T 视为一个整体检查
Ref
一张图解释vue3中的响应式逻辑
从图中可以看出,Ref<T> 是最通用的响应式容器,既能处理基础类型,也能处理对象类型。而 Reactive<T> 专门处理对象,ComputedRef<T> 是特殊的只读 Ref<T>。
实际使用中的类型特性
1. 自动解包(模板中)
vue
<template>
<!-- 模板中自动解包,不需要 .value -->
<div>{{ count }}</div> <!-- 显示 0,而不是 {value: 0} -->
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0) // Ref<number>
</script>
2. 响应式类型守卫
typescript
import { ref, isRef, unref } from 'vue'
function processValue(input: number | Ref<number>) {
// 类型守卫:判断是否为 Ref
if (isRef(input)) {
// 此处 TypeScript 知道 input 是 Ref<number>
return input.value * 2
}
// 此处 TypeScript 知道 input 是 number
return input * 2
// 或者使用 unref 自动解包
const value = unref(input) // number
}
3. 在响应式对象中的自动解包【重要】
typescript
import { reactive, ref } from 'vue'
const count = ref(0)
const obj = reactive({
count, // 自动解包为 number
normal: 1
})
console.log(obj.count) // 0 (number,不是 Ref<number>)
console.log(obj.normal) // 1
4. 源码分析
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
/**
* @internal
*/
class RefImpl<T = any> {
_value: T
private _rawValue: T
dep: Dep = new Dep()
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false
constructor(value: T, isShallow: boolean) {
this._rawValue = isShallow ? value : toRaw(value)
this._value = isShallow ? value : toReactive(value)
this[ReactiveFlags.IS_SHALLOW] = isShallow
}
get value() {
if (__DEV__) {
this.dep.track({
target: this,
type: TrackOpTypes.GET,
key: 'value',
})
} else {
this.dep.track()
}
return this._value
}
set value(newValue) {
const oldValue = this._rawValue
const useDirectValue =
this[ReactiveFlags.IS_SHALLOW] ||
isShallow(newValue) ||
isReadonly(newValue)
newValue = useDirectValue ? newValue : toRaw(newValue)
if (hasChanged(newValue, oldValue)) {
this._rawValue = newValue
this._value = useDirectValue ? newValue : toReactive(newValue)
if (__DEV__) {
this.dep.trigger({
target: this,
type: TriggerOpTypes.SET,
key: 'value',
newValue,
oldValue,
})
} else {
this.dep.trigger()
}
}
}
}
- 总结:
ref的对于基本类型的响应式处理就是包装成了一个具有value属性的对象,在访问和值变化的时候进行依赖收集和触发更新;即.value 只是普通对象属性 + getter/setter - 这种设计非常巧妙:
- 具有统一性
// ref 可以包装任何类型,API 统一
const num = ref(0) // Ref<number>
const obj = ref({ x: 1 }) // Ref<{x: number}>(内部用 reactive 包装)
const arr = ref([1, 2, 3]) // Ref<number[]>
// 都是 .value 访问,心智负担小
- 可预测性
const count = ref(0)
// 明确的触发点:只有 .value 赋值会触发更新
count.value = 1 // ✅ 触发
count.value++ // ✅ 触发
// 没有意外的触发
const obj = ref({ x: 1 })
obj.value.x = 2 // ❌ 不触发(除非是 reactive 包装的深层 ref)
- 类型安全
// TypeScript 完美支持
const count = ref(0) // 推断为 Ref<number>
count.value = "hello" // ❌ 类型错误:不能将 string 赋值给 number
// 泛型支持
const maybe = ref<number | null>(null) // Ref<number | null>
maybe.value = 42 // ✅
maybe.value = null // ✅
maybe.value = "text" // ❌
- 从Vue3 Ref的这段源码[RefImpl的源码]可以学习到类的分层级的访问控制策略
- 为什么这样分层设计?
-
_rawValue最严格:- 存储原始值用于
hasChanged()比较 - 如果被外部修改,响应式系统会完全失效
- 必须用
private绝对保护
- 存储原始值用于
-
_value较宽松:- 存储响应式值(可能是
reactive()代理) - 外部访问可能看到代理对象,但不破坏核心逻辑
- 开发工具可能需要检查这个值
- 存储响应式值(可能是
-
dep完全隐藏:- 是实现机制,不是数据模型
- 用户永远不需要直接操作依赖关系
- 通过不导出类来彻底隐藏