文章目录
- 一、文件概览
- 二、核心数据结构
-
- [1. Ref 接口定义](#1. Ref 接口定义)
- 三、核心函数实现
-
- [1. isRef - 类型守卫](#1. isRef - 类型守卫)
-
- [2. `r[ReactiveFlags.IS_REF]`详解](#2.
r[ReactiveFlags.IS_REF]详解) -
- [一、 `r[ReactiveFlags.IS_REF]` 是什么意思?](#一、
r[ReactiveFlags.IS_REF]是什么意思?) - 二、这个标记是怎么来的?
- 三、为什么需要这个标记?
- 四、完整的标记系统
- [五、为什么用不 Symbol 而用字符串?](#五、为什么用不 Symbol 而用字符串?)
- 六、手写简化版来理解
- 七、总结
- [一、 `r[ReactiveFlags.IS_REF]` 是什么意思?](#一、
- [2. `r[ReactiveFlags.IS_REF]`详解](#2.
- [2. ref - 主入口函数](#2. ref - 主入口函数)
-
- 源码解析
-
- [2.1 [T] extends [Ref] 详解](#2.1 [T] extends [Ref] 详解)
- [2.2 条件分支一`IfAny<T, Ref<T>, T>`详解](#2.2 条件分支一
IfAny<T, Ref<T>, T>详解) - [2.3 条件分支二`Ref<UnwrapRef<T>, UnwrapRef<T> | T>`详解](#2.3 条件分支二
Ref<UnwrapRef<T>, UnwrapRef<T> | T>详解)
- [3. shallowRef - 浅层响应式 ref 的实现](#3. shallowRef - 浅层响应式 ref 的实现)
一、文件概览
| 元数据 | 说明 |
|---|---|
| 文件路径 | packages/reactivity/src/ref.ts |
| 核心功能 | 实现 Vue 3 的 ref 相关 API |
| 依赖模块 | @vue/shared, ./dep,./reactive |
| 导出 API | ref, shallowRef, isRef, unref, toRef, toRefs, customRef, triggerRef |
二、核心数据结构
1. Ref 接口定义
export interface Ref<T = any, S = T> {
get value(): T // 读取时返回类型 T
set value(_: S) // 写入时接受类型 S(默认等于 T)
[RefSymbol]: true // 唯一类型标记,用于类型识别
}
我的理解:
- 使用两个泛型参数
T和S是为了支持只读场景(S = never) [RefSymbol]: true是一个类型层面的标记,编译后消失- 为什么这样设计? 这样可以精确控制 ref 的读写类型,比如计算属性可以是只读的
三、核心函数实现
1. isRef - 类型守卫
export function isRef(r: any): r is Ref {
return r ? r[ReactiveFlags.IS_REF] === true : false
}
源码解析:
- 类型谓词
r is Ref:告诉 TypeScript,如果返回 true,则 r 是 Ref 类型 - 内部标记
ReactiveFlags.IS_REF = '__v_isRef':每个 ref 实例上都有这个属性 - 为什么不用 instanceof? 因为对象可能被 Proxy 代理,原型链会丢失
笔记:
// 使用示例
const count = ref(0)
console.log(isRef(count)) // true
console.log(isRef(100)) // false
2. r[ReactiveFlags.IS_REF]详解
一、 r[ReactiveFlags.IS_REF] 是什么意思?
1. 基本概念
// packages/reactivity/src/constants.ts
export enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw',
IS_REF = '__v_isRef',
}
r[ReactiveFlags.IS_REF] === true
// 等价于
r['__v_isRef'] === true
这行代码的意思是:检查对象 r 上是否有 __v_isRef **这个属性,并且它的值是否为 ** true。
2. 为什么用 __v_isRef 这种奇怪的属性名?
__v_是 Vue 内部属性的命名约定(v 代表 Vue)- 这样做是为了避免和用户自定义的属性名冲突
- 用户几乎不可能恰好定义一个叫
__v_isRef的属性
二、这个标记是怎么来的?
在 RefImpl 类中设置的
// packages/reactivity/src/ref.ts
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined
// 重点在这里!这个标记是在创建 ref 时自动添加的
public readonly [ReactiveFlags.IS_REF] = true // <-- 就是这里!
constructor(value: T) {
this._value = value
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = newVal
triggerRefValue(this)
}
}
}
每个 ref 对象在创建时,都会被自动加上 __v_isRef: true 这个属性。
三、为什么需要这个标记?
1. 快速识别 ref 对象
// 假设没有这个标记,怎么判断是不是 ref?
function isRef(r: any) {
// 方法1:检查构造函数?但 Proxy 代理后不行
// 方法2:检查有没有 value 属性?但普通对象也可能有 value
// 方法3:检查内部属性?最可靠!
return r && r.__v_isRef === true
}
2. 运行时快速判断
const count = ref(0)
const obj = { value: 100 } // 普通对象,碰巧也有 value
console.log(isRef(count)) // true,因为有 __v_isRef
console.log(isRef(obj)) // false,没有 __v_isRef
console.log(isRef(null)) // false
3. 在响应式系统中做特殊处理
// 在 reactive 中遇到 ref 时,会自动解包
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 重点:如果获取到的值是一个 ref,自动返回 .value
if (isRef(res)) {
return res.value // 自动解包!
}
return res
}
})
}
// 使用时的效果
const count = ref(0)
const state = reactive({
count: count // 传入 ref
})
console.log(state.count) // 0,直接拿到值,不需要 .value!
// 这全靠 __v_isRef 标记来判断
四、完整的标记系统
Vue 用了多个类似的标记:
export enum ReactiveFlags {
SKIP = '__v_skip', // 跳过响应式转换
IS_REACTIVE = '__v_isReactive', // 是否是 reactive 对象
IS_READONLY = '__v_isReadonly', // 是否是 readonly 对象
IS_SHALLOW = '__v_isShallow', // 是否是 shallow 对象
RAW = '__v_raw', // 获取原始对象
IS_REF = '__v_isRef', // 是否是 ref 对象
}
其他标记的使用
// reactive 对象也有自己的标记
class ReactiveProxy {
public readonly [ReactiveFlags.IS_REACTIVE] = true
public readonly [ReactiveFlags.RAW] = this.target
// ...
}
// 使用
const state = reactive({ count: 0 })
console.log(state[ReactiveFlags.IS_REACTIVE]) // true
console.log(isReactive(state)) // true,内部就是检查这个标记
五、为什么用不 Symbol 而用字符串?
你可能会想:为什么不用 Symbol 而用字符串?
// 理论上可以用 Symbol
export const IS_REF = Symbol('vue.isRef')
// 但 Vue 选择了字符串,为什么?
字符串的好处:
-
可序列化:字符串可以在 JSON 中传输
-
调试友好 :
__v_isRef在控制台直接可见 -
跨框架边界:如果 ref 传到其他框架,字符串属性仍然存在
-
简单可靠:兼容性更好
// 控制台直接查看
const count = ref(0)
console.log(count) // 可以直接看到 __v_isRef: true
// 如果是 Symbol,控制台显示 [Symbol()]: true,可读性差
六、手写简化版来理解
// 1. 定义标记常量
const IS_REF = '__v_isRef'
// 2. 实现 ref
class MyRef<T> {
private _value: T
// 添加标记
readonly [IS_REF] = true
constructor(value: T) {
this._value = value
}
get value() {
return this._value
}
set value(newVal) {
this._value = newVal
}
}
// 3. 实现 isRef
function isRef(r: any): boolean {
return r && r[IS_REF] === true
}
// 4. 使用
const myRef = new MyRef(100)
console.log(isRef(myRef)) // true
const normalObj = { value: 100 }
console.log(isRef(normalObj)) // false
七、总结
| 问题 | 答案 |
|---|---|
r[ReactiveFlags.IS_REF] 是什么? |
访问 ref 对象上的内部标记 __v_isRef |
| 为什么要判断这个属性? | 快速、可靠地判断一个对象是不是 ref |
| 为这个属性哪里来的? | RefImpl 类在创建实例时自动添加的 |
为什么不用 instanceof? |
Proxy 代理后会丢失原型链,而且跨 iframe 不工作 |
| 为什么用字符串? | 可序列化、调试友好、简单可靠 |
通俗理解:
这就像给每个 ref 对象贴了一个"我是 ref"的防伪标签。isRef 函数就是检查这个标签存不存在、是不是真。这种方式比 instanceof 更可靠,因为即使对象被 Proxy 代理、被传到不同的 iframe、甚至被序列化后再解析,这个字符串属性依然存在。
2. ref - 主入口函数
export function ref<T>(
value: T,
): [T] extends [Ref] ? IfAny<T, Ref<T>, T> : Ref<UnwrapRef<T>, UnwrapRef<T> | T>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
return createRef(value, false) // 第二个参数 false 表示非浅层 ref
}
源码解析
| 重载 | 作用 |
|---|---|
| 第一个 | 处理传入值的情况,防止嵌套 ref |
| 第二个 | 处理不传参的情况,创建值为 undefined 的 ref |
| 实现体 | 统一调用 createRef |
重点理解:
[T] extends [Ref]:检查 T 是否已经是 Ref 类型,避免创建Ref<Ref>IfAny<T, Ref<T>, T>:如果 T 是 any,返回 Ref,否则返回 T 本身UnwrapRef<T>:递归解包嵌套的 ref
2.1 [T] extends [Ref] 详解
[T] extends [Ref] 是什么意思?
它是TS的条件类型,意思是:判断类型 T 是否是 Ref 类型。
1. 基本语法
[T] extends [Ref] ? A : B
2. 为什么用 [T] 而不是 T?
目的:避免联合类型的分发,做严格的一次性判断
// 如果直接写 T extends Ref
type Test1<T> = T extends Ref ? true : false
// 当 T = string | Ref 时,会分别判断:
// string extends Ref? false
// Ref extends Ref? true
// 结果:boolean(联合类型)
// 用 [T] extends [Ref]
type Test2<T> = [T] extends [Ref] ? true : false
// 当 T = string | Ref 时,作为整体判断
// [string | Ref] 是不是 [Ref] 的子类型?否
// 结果:false
2.2 条件分支一IfAny<T, Ref<T>, T>详解
我的追问:
为什么
[T] extends [Ref]为true,不就说明T是Ref类型了吗,为什么还需要判断T是否是any,并给any类型包装Ref,这样不会造成重复包装吗?
关键在于 :[T] extends [Ref] 为 true 时,T 可能是 Ref,也可能是 any
为什么会有 any 的情况?
// 场景:传入 any
const value: any = 123
const refAny = ref(value) // T = any
// 此时判断 [any] extends [Ref] 是 true 还是 false?
TypeScript 中 any 的特殊性:
// any 可以赋值给任何类型
let x: any = 123
let y: string = x // ✅ 允许,any 可以赋值给 string
// 所以:
type Test = [any] extends [Ref] ? true : false // true!
// 因为 any 可以当作 Ref 来用(尽管实际不是)
用代码验证
// 写个简单的类型测试
type IsRef<T> = [T] extends [Ref] ? true : false
// 测试1:真正的 ref
type Test1 = IsRef<Ref<number>> // true ✅
// 测试2:any
type Test2 = IsRef<any> // true ✅(any 万能匹配)
// 测试3:普通类型
type Test3 = IsRef<number> // false
type Test4 = IsRef<string> // false
问题就在这里 : any 会让 [T] extends [Ref] 也返回 true!
IfAny 的作用
// IfAny 的定义(来自 @vue/shared)
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N
// 这是一个判断 T 是否是 any 的类型工具
如果不判断 any
// 假设没有 IfAny,直接返回 T
function badRef<T>(value: T): [T] extends [Ref] ? T : Ref<T> {
// ...
}
const value: any = 123
const result = badRef(value) // 返回类型:any
// 类型丢失!我们不知道这是个 ref 了
为什么不能直接返回 Ref?
// 如果直接返回 Ref<T>
function badRef<T>(value: T): [T] extends [Ref] ? Ref<T> : Ref<T> {
// ...
}
// 问题1:传入真正的 ref 时
const count = ref(0)
const result = badRef(count) // Ref<Ref<number>> ❌ 嵌套了!
// 问题2:传入 any 时
const val: any = 123
const result2 = badRef(val) // Ref<any> ✅ 这个是对的
// 问题3:传入普通值时
const num = 123
const result3 = badRef(num) // Ref<number> ✅ 这个也是对的
所以需要分支处理:
- 传入真正 ref 时 → 返回 T 本身(防止嵌套)
- 传入 any 时 → 返回 Ref(保留 ref 信息)
总结
| 传入值 | T 的类型 | [T] extends [Ref] | IfAny 结果 | 最终类型 |
|---|---|---|---|---|
ref(0) |
Ref<number> |
true | 不是 any → 返回 T | Ref<number> |
any 值 |
any |
true | 是 any → 返回 Ref<any> |
Ref<any> |
123 |
number |
false | - | Ref<number> |
关键点:
[T] extends [Ref]为 true 时,T 可能是真正的 ref,也可能是 anyIfAny用来区分这两种情况- 真正 ref 要返回本身(防止嵌套)
- any 要包装成 Ref(保留信息)
2.3 条件分支二Ref<UnwrapRef<T>, UnwrapRef<T> | T>详解
[T] extends [Ref] 为 false(T 不是 Ref)
: Ref<UnwrapRef<T>, UnwrapRef<T> | T>
// 创建一个 Ref,值的类型要经过 UnwrapRef 处理
Ref<UnwrapRef<T>, UnwrapRef<T> | T> 的含义
Ref<UnwrapRef<T>, UnwrapRef<T> | T>
// 第一个参数(读取类型):UnwrapRef<T>
// 第二个参数(写入类型):UnwrapRef<T> | T
UnwrapRef 是什么?
定义
export type UnwrapRef<T> =
T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>
UnwrapRef 的作用:递归地解包嵌套的 ref
// 基本类型
type A = UnwrapRef<number> // number
type B = UnwrapRef<string> // string
// 一层 ref
type C = UnwrapRef<Ref<number>> // number(解包了)
// 嵌套 ref
type D = UnwrapRef<Ref<Ref<number>>> // number(递归解包)
// 对象中的 ref
type E = UnwrapRef<{
count: Ref<number>
}> // { count: number }(对象属性也被解包)
UnwrapRef 定义详解
先看整体结构,这是 TypeScript 的递归类型解包
export type UnwrapRef<T> =
T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>
这是一个条件类型,意思是:
- 如果 T 是一个 Ref 类型,就解包出它里面的值 V,然后递归处理 V
- 如果 T 不是 Ref 类型,就直接用 UnwrapRefSimple 处理 T
拆解关键语法
T extends Ref<infer V>
这是 TypeScript 的模式匹配语法
// 假设有一个 Ref 类型
type Ref<T> = { value: T }
// infer V 的意思是:把 Ref 里面的类型提取出来,命名为 V
type GetRefValue<T> = T extends Ref<infer V> ? V : never
// 使用
type A = GetRefValue<Ref<number>> // number(提取出来了)
type B = GetRefValue<string> // never(不是 Ref)
UnwrapRefSimple<V>
这是另一个类型工具,用来处理非 Ref 类型的解包(比如对象、数组等)
执行流程示例
例子1:基本类型
type T1 = UnwrapRef<number>
// number 不是 Ref → 走第二支:UnwrapRefSimple<number>
// 结果:number
例子2:一层 Ref
type T2 = UnwrapRef<Ref<number>>
// Ref<number> 是 Ref → 走第一支:UnwrapRefSimple<number>
// 结果:number
例子3:嵌套 Ref
type T3 = UnwrapRef<Ref<Ref<number>>>
// 第一次:Ref<Ref<number>> 是 Ref → 提取 V = Ref<number>
// 调用 UnwrapRefSimple<Ref<number>>
// UnwrapRefSimple 内部又会调用 UnwrapRef(递归)
// 第二次:Ref<number> 是 Ref → 提取 V = number
// 调用 UnwrapRefSimple<number>
// 结果:number
例子4:对象包含 Ref
type T4 = UnwrapRef<{ count: Ref<number> }>
// { count: Ref<number> } 不是 Ref → 走第二支
// UnwrapRefSimple<{ count: Ref<number> }>
// UnwrapRefSimple 会遍历对象属性,对每个属性递归调用 UnwrapRef
// 最终:{ count: number }
配合 UnwrapRefSimple 看
export type UnwrapRefSimple<T> =
T extends Builtin ? T : // 基本类型直接返回
T extends Map<infer K, infer V> ? Map<K, UnwrapRefSimple<V>> : // Map 特殊处理
T extends Set<infer V> ? Set<UnwrapRefSimple<V>> : // Set 特殊处理
T extends object ? { [P in keyof T]: UnwrapRef<T[P]> } : // 对象递归解包
T
完整流程示例
type Test = UnwrapRef<{
count: Ref<number>,
user: {
name: Ref<string>,
age: number
},
tags: Ref<Set<Ref<string>>>
}>
// 执行过程:
// 1. { count: Ref... } 不是 Ref → UnwrapRefSimple
// 2. 遍历对象属性:
// - count: UnwrapRef<Ref<number>> → number
// - user: UnwrapRef<{ name: Ref<string>, age: number }>
// → 递归处理 user 对象
// * name: UnwrapRef<Ref<string>> → string
// * age: number → number
// - tags: UnwrapRef<Ref<Set<Ref<string>>>>
// → 提取 Set<Ref<string>>
// → UnwrapRefSimple<Set<Ref<string>>>
// → 处理 Set:Set<UnwrapRefSimple<Ref<string>>>
// → UnwrapRefSimple<Ref<string>> → string
// → 最终:Set<string>
// 结果:
{
count: number,
user: {
name: string,
age: number
},
tags: Set<string>
}
为什么需要递归解包?
没有递归解包导致的问题
// 假设只有一层解包
type ShallowUnwrap<T> = T extends Ref<infer V> ? V : T
const obj = ref({
user: ref({
name: ref('vue')
})
})
// 使用时:
obj.value.user // 类型还是 Ref<{ name: Ref<string> }> ❌
obj.value.user.value.name.value // 要写一堆 .value
有递归解包
const obj = ref({
user: ref({
name: ref('vue')
})
})
// 使用时:
obj.value.user.name // 直接是 string!✅
// 所有层级的 ref 都被自动解开了
类比理解
把 UnwrapRef 想象成一个剥洋葱的过程
// 洋葱:Ref<Ref<Ref<number>>>
UnwrapRef<Ref<Ref<Ref<number>>>>
// 第一层:发现是 Ref → 剥开,得到 Ref<Ref<number>>
// 第二层:发现还是 Ref → 再剥开,得到 Ref<number>
// 第三层:发现还是 Ref → 再剥开,得到 number
// 结果:number
把 UnwrapRefSimple 想象成处理各种食材的工具:
- 基本类型(Builtin)→ 直接吃
- Map/Set → 特殊处理
- 对象 → 每个属性都剥一遍
- 其他 → 保持原样
回到Ref<UnwrapRef<T>, UnwrapRef<T> | T>
Ref<UnwrapRef<T>, UnwrapRef<T> | T>
// 第一个参数(读取类型):UnwrapRef<T>
// 第二个参数(写入类型):UnwrapRef<T> | T
为什么写入类型要允许两种?
看例子理解
例子:传入普通值
const count = ref(0) // T = number
// Ref<UnwrapRef<number>, UnwrapRef<number> | number>
// = Ref<number, number | number>
// = Ref<number, number>
count.value = 10 // ✅ 写入 number
count.value = '20' // ❌ 类型错误,只能写 number
例子:赋值带 ref 的对象
const state = ref({
name: ref('vue'),
age: ref(3)
})
// 类型:Ref<{ name: string, age: number },
// { name: string, age: number } | { name: Ref<string>, age: Ref<number> }>
// 读取时自动解包
console.log(state.value.name) // string
// 写入时两种都支持:
state.value = { name: 'react', age: 10 } // ✅ 普通对象
state.value = { name: ref('angular'), age: ref(5) } // ✅ 带 ref 的对象
如果不这样设计会怎样?
方案A:只允许 UnwrapRef
Ref<UnwrapRef<T>, UnwrapRef<T>> // 写入只能写解包后的类型
const obj = ref({ count: ref(0) })
obj.value = { count: 10 } // ✅ 可以
obj.value = { count: ref(20) } // ❌ 类型错误,不能写 ref
// 但有时我们需要写入 ref,比如从另一个 ref 赋值
方案B:只允许 T
Ref<UnwrapRef<T>, T> // 写入只能写原始类型
const obj = ref({ count: ref(0) })
obj.value = { count: ref(20) } // ✅ 可以
obj.value = { count: 10 } // ❌ 类型错误,不能写普通值
// 但大多数时候我们从 API 拿到的是普通对象
Vue 的方案:两者都允许
Ref<UnwrapRef<T>, UnwrapRef<T> | T> // 两种都支持
// 既可以从 API 赋值普通对象
// 也可以从其他 ref 赋值
总结
| 参数 | 作用 | 为什么这样设计 |
|---|---|---|
UnwrapRef<T> |
读取类型 | 让使用者直接拿到解包后的值,方便使用 |
UnwrapRef<T> 或 T |
写入类型 | 既支持普通对象,也支持带 ref 的对象,更灵活 |
通俗理解:
- 读取时:Vue 帮你把里面的 ref 都解开了,你直接拿值用
- 写入时:Vue 很宽容,你想传普通对象也行,想传带 ref 的对象也行
这就是 Vue 响应式系统"读取方便,写入灵活"的设计哲学!
3. shallowRef - 浅层响应式 ref 的实现
持续更新中,未完待续~