Vue 3 源码阅读笔记:ref.ts

文章目录

一、文件概览

元数据 说明
文件路径 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   // 唯一类型标记,用于类型识别
}

我的理解:

  • 使用两个泛型参数 TS 是为了支持只读场景(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 选择了字符串,为什么?

字符串的好处:

  1. 可序列化:字符串可以在 JSON 中传输

  2. 调试友好__v_isRef 在控制台直接可见

  3. 跨框架边界:如果 ref 传到其他框架,字符串属性仍然存在

  4. 简单可靠:兼容性更好

    // 控制台直接查看
    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,不就说明TRef类型了吗,为什么还需要判断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,也可能是 any
  • IfAny 用来区分这两种情况
  • 真正 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

拆解关键语法

  1. 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)
  1. 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 的实现

持续更新中,未完待续~

相关推荐
嘉琪0012 小时前
Day4 完整学习包(this 指向)——2026 0313
前端·javascript·学习
前端小菜鸟也有人起2 小时前
Vue3父子组件通信方法总结
前端·javascript·vue.js
Maimai108082 小时前
React Server Components 是什么?一文讲清 CSR、Server Components 与 Next.js 中的客户端/服务端组件
前端·javascript·css·react.js·前端框架·html·web3
程序员夏末2 小时前
【LeetCode | 第四篇】算法笔记
笔记·算法·leetcode
肉肉不吃 肉2 小时前
事件循环,宏任务,微任务
前端·javascript
z止于至善2 小时前
Vue ECharts:Vue 生态下的 ECharts 可视化最佳实践
前端·vue.js·echarts·vue echarts
Software攻城狮2 小时前
【el-table 表格组件 删除标头分割线】
前端·vue.js·elementui