vue3 ref解析

23 阅读4分钟

在 Vue3 中,ref 是响应式系统的核心 API 之一,专门解决原始值(Number、String、Boolean 等) 无法被 Proxy 拦截的问题。我会从「设计初衷」「源码实现」「核心特性」「使用场景」四个维度,彻底解析 ref,帮你理解它的底层逻辑和使用细节。

一、为什么需要 ref?

Vue3 的 reactive 基于 Proxy 实现,但 Proxy 只能拦截对象 / 数组的属性访问,无法直接拦截原始值(因为原始值不是引用类型,没有属性可拦截)。

举个例子:

// 原始值无法被 reactive 响应式化
let num = reactive(1); 
num = 2; // 只是重新赋值变量,不会触发更新

// 而 ref 可以解决这个问题
let num = ref(1);
num.value = 2; // 会触发响应式更新

ref 的核心设计思路:把原始值包裹成一个对象,通过访问器属性(get/set)拦截 .value 的读写,从而实现响应式

二、ref 核心源码解析

ref 的源码主要在 packages/reactivity/src/ref.ts 中,核心逻辑分为「创建 ref 实例」「依赖收集」「触发更新」三部分。

1. 核心入口:ref 函数

// 对外暴露的 ref 函数
export function ref<T>(value: T): Ref<UnwrapRef<T>> {
  return createRef(value, false)
}

// 内部核心创建函数(区分 shallowRef)
function createRef(rawValue: unknown, shallow: boolean) {
  // 避免重复包装:如果已经是 ref,直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  // 核心:创建 RefImpl 实例(真正实现响应式的类)
  return new RefImpl(rawValue, shallow)
}

2. 核心实现:RefImpl 类

这是 ref 的核心,通过类的访问器属性(get value()/set value())拦截 .value 的读写:

class RefImpl<T> {
  // 私有属性:存储原始值和处理后的响应式值
  private _value: T
  private _rawValue: T

  // 标记:标识这是一个 ref(供 isRef 检测)
  public readonly __v_isRef = true

  constructor(value: T, public readonly _shallow: boolean) {
    // 1. 保存原始值(用于后续对比是否变化)
    this._rawValue = toRaw(value)
    // 2. 处理值:shallow 为 false 时,深层响应式(如 ref({a:1}) 会转 reactive)
    this._value = _shallow ? value : toReactive(value)
  }

  // 读取 .value 时触发(依赖收集)
  get value() {
    // 核心:收集依赖(和 reactive 的 track 逻辑一致)
    trackRefValue(this)
    // 返回处理后的值
    return this._value
  }

  // 赋值 .value 时触发(触发更新)
  set value(newVal) {
    // 转原始值,避免响应式对象对比出错
    newVal = this._shallow ? newVal : toRaw(newVal)
    // 只有值真正变化时,才更新并触发更新
    if (hasChanged(newVal, this._rawValue)) {
      // 更新原始值和响应式值
      this._rawValue = newVal
      this._value = this._shallow ? newVal : toReactive(newVal)
      // 核心:触发依赖更新
      triggerRefValue(this, newVal)
    }
  }
}

3. 关键辅助函数

  • toReactive:如果值是对象,自动转为 reactive(所以 ref({a:1}) 等价于 ref(reactive({a:1}))):

    export const toReactive = <T extends unknown>(value: T): T => {
      return isObject(value) ? reactive(value) : value
    }
    
  • hasChanged:判断值是否真的变化(处理 NaN、引用类型等特殊情况):

    export const hasChanged = (value: any, oldValue: any): boolean => {
      return !Object.is(value, oldValue)
    }
    
  • trackRefValue/triggerRefValue:专门针对 ref 的依赖收集和触发更新,底层复用了 track/trigger 逻辑。

三、ref 的核心特性

1. 自动解包(模板中无需 .value)

在模板中使用 ref 时,Vue 会自动解包,无需写 .value

<template>
  <!-- 直接写 num,等价于 num.value -->
  <div>{{ num }}</div>
</template>

<script setup>
import { ref } from 'vue'
const num = ref(1)
</script>

源码逻辑:在组件渲染时,Vue 会遍历 setup 返回的对象,对 ref 类型的属性做「自动解包」处理(访问属性时自动取 .value)。

2. 嵌套对象的响应式

如果 ref 的值是对象 / 数组,会被 toReactive 转为 reactive,因此嵌套属性也能响应式:

const objRef = ref({ a: 1 })
objRef.value.a = 2 // 会触发更新(因为内部是 reactive)

3. ref 与 reactive 的互操作

  • ref 作为 reactive 对象的属性时,会自动解包:

    const numRef = ref(1)
    const obj = reactive({ num: numRef })
    console.log(obj.num) // 1(自动解包,无需 .value)
    obj.num = 2 // 等价于 numRef.value = 2,会触发更新
    
  • 数组 / Map 中的 ref 不会自动解包:

    const arr = reactive([ref(1)])
    console.log(arr[0].value) // 必须写 .value
    

四、ref 的衍生 API

1. shallowRef

浅响应式 ref,只监听 .value 的赋值,不监听内部对象的变化:

export function shallowRef<T>(value: T): Ref<T> {
  return createRef(value, true) // _shallow 为 true
}

// 使用示例
const objRef = shallowRef({ a: 1 })
objRef.value.a = 2 // 不会触发更新(只监听 .value 赋值)
objRef.value = { a: 2 } // 会触发更新

2. isRef

检测是否为 ref 实例(通过 __v_isRef 标记):

export function isRef<T>(r: Ref<T> | unknown): r is Ref<T> {
  return Boolean(r && (r as Ref).__v_isRef === true)
}

3. unref

语法糖,unref(x) 等价于 isRef(x) ? x.value : x

export function unref<T>(ref: T | Ref<T>): T {
  return isRef(ref) ? ref.value : ref
}

总结

  1. 核心本质:ref 是「原始值的响应式包装器」,通过类的访问器属性拦截 .value 的读写,解决原始值无法被 Proxy 拦截的问题;
  2. 关键逻辑:创建 RefImpl 实例 → get value 时收集依赖 → set value 时触发更新;
  3. 核心特性:模板自动解包、对象值自动转 reactive、与 reactive 互操作时的部分自动解包。

理解 ref 的核心是记住:ref 所有的响应式都围绕 .value 展开,无论是原始值还是对象,只有操作 .value(或模板自动解包)才会触发响应式更新。