在 Vue3 中,ref 是响应式系统的核心 API 之一,专门解决原始值(Number、String、Boolean 等) 无法被 Proxy 拦截的问题。我会从「设计初衷」「源码实现」「核心特性」「使用场景」四个维度,彻底解析 ref,帮你理解它的底层逻辑和使用细节。
一、为什么需要 ref?
Vue3 的 reactive 基于 Proxy 实现,但 Proxy 只能拦截对象 / 数组 的属性访问,无法直接拦截原始值(因为原始值不是引用类型,没有属性可拦截)。
举个例子:
ini
// 原始值无法被 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 函数
php
// 对外暴露的 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 的读写:
kotlin
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}))):typescriptexport const toReactive = <T extends unknown>(value: T): T => { return isObject(value) ? reactive(value) : value } -
hasChanged:判断值是否真的变化(处理 NaN、引用类型等特殊情况):typescriptexport const hasChanged = (value: any, oldValue: any): boolean => { return !Object.is(value, oldValue) } -
trackRefValue/triggerRefValue:专门针对 ref 的依赖收集和触发更新,底层复用了track/trigger逻辑。
三、ref 的核心特性
1. 自动解包(模板中无需 .value)
在模板中使用 ref 时,Vue 会自动解包,无需写 .value:
xml
<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,因此嵌套属性也能响应式:
ini
const objRef = ref({ a: 1 })
objRef.value.a = 2 // 会触发更新(因为内部是 reactive)
3. ref 与 reactive 的互操作
-
ref 作为 reactive 对象的属性时,会自动解包:
scssconst numRef = ref(1) const obj = reactive({ num: numRef }) console.log(obj.num) // 1(自动解包,无需 .value) obj.num = 2 // 等价于 numRef.value = 2,会触发更新 -
数组 / Map 中的 ref 不会自动解包:
scssconst arr = reactive([ref(1)]) console.log(arr[0].value) // 必须写 .value
四、ref 的衍生 API
1. shallowRef
浅响应式 ref,只监听 .value 的赋值,不监听内部对象的变化:
php
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 标记):
sql
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:
csharp
export function unref<T>(ref: T | Ref<T>): T {
return isRef(ref) ? ref.value : ref
}
总结
- 核心本质 :ref 是「原始值的响应式包装器」,通过类的访问器属性拦截
.value的读写,解决原始值无法被 Proxy 拦截的问题; - 关键逻辑:创建 RefImpl 实例 → get value 时收集依赖 → set value 时触发更新;
- 核心特性:模板自动解包、对象值自动转 reactive、与 reactive 互操作时的部分自动解包。
理解 ref 的核心是记住:ref 所有的响应式都围绕 .value 展开 ,无论是原始值还是对象,只有操作 .value(或模板自动解包)才会触发响应式更新。