响应式记录

源码中的TS类型记录

类型谓词is的使用(TS类型收窄方式)

  • TS类型收窄方式

类型谓词是"用户自定义类型保护"中唯一且核心的语法形式

IfAny

typescript

swift 复制代码
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 类型的特殊"传染性"。

为什么[T] extends [Ref]不写成T extends Ref

  • 核心区别:是否触发"分布式条件类型"
  • 这是 TypeScript 条件类型的一个高级特性 。当 extends 左侧是裸类型参数时,会触发"分布式条件类型",否则不会。

typescript

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

xml 复制代码
<template>
  <!-- 模板中自动解包,不需要 .value -->
  <div>{{ count }}</div> <!-- 显示 0,而不是 {value: 0} -->
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0) // Ref<number>
</script>
2. 响应式类型守卫

typescript

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

javascript 复制代码
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
  1. 源码分析
ts 复制代码
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
  • 这种设计非常巧妙:
    • 具有统一性
csharp 复制代码
// 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 访问,心智负担小
  • 可预测性
csharp 复制代码
const count = ref(0)

// 明确的触发点:只有 .value 赋值会触发更新
count.value = 1  // ✅ 触发
count.value++    // ✅ 触发

// 没有意外的触发
const obj = ref({ x: 1 })
obj.value.x = 2  // ❌ 不触发(除非是 reactive 包装的深层 ref)
  • 类型安全
csharp 复制代码
// 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的这段源码可以学习到类的分层级的访问控制策略
  • 为什么这样分层设计?
  1. _rawValue 最严格

    • 存储原始值 用于 hasChanged() 比较
    • 如果被外部修改,响应式系统会完全失效
    • 必须用 private 绝对保护
  2. _value 较宽松

    • 存储响应式值 (可能是 reactive() 代理)
    • 外部访问可能看到代理对象,但不破坏核心逻辑
    • 开发工具可能需要检查这个值
  3. dep 完全隐藏

    • 实现机制 ,不是数据模型
    • 用户永远不需要直接操作依赖关系
    • 通过不导出类来彻底隐藏
相关推荐
北辰alk1 小时前
Vue打包后静态资源图片失效?一网打尽所有解决方案!
vue.js
干就完了11 小时前
关于git的操作命令(一篇盖全),可不用,但不可不知!
前端·javascript
hjt_未来可期1 小时前
js实现替换输入框中选中的文字
javascript·vue.js
之恒君1 小时前
JavaScript 垃圾回收机制详解
前端·javascript
是你的小橘呀1 小时前
像前任一样捉摸不定的异步逻辑,一文让你彻底看透——JS 事件循环
前端·javascript·面试
Cache技术分享1 小时前
260. Java 集合 - 深入了解 HashSet 的内部结构
前端·后端
前端老宋Running1 小时前
你的代码在裸奔?给 React 应用穿上“防弹衣”的保姆级教程
前端·javascript·程序员
汤姆Tom1 小时前
前端转战后端:JavaScript 与 Java 对照学习指南(第四篇 —— List)
前端·编程语言·全栈
FinClip1 小时前
当豆包手机刷屏时,另一场“静悄悄”的变革已经在你手机里发生
前端