vue3 ref解析

在 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}))):

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

    typescript 复制代码
    export 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 对象的属性时,会自动解包:

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

    scss 复制代码
    const 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
}

总结

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

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

相关推荐
蜗牛前端9 小时前
codex 全流程开发上线的高颜值礼簿小程序
前端·微信小程序
大龄秃头程序员10 小时前
我在图文流 App 里落地双层缓存、弱网降级与 OOM 治理
前端
老王以为10 小时前
React Renderer 分离的多平台架构
前端·react native·react.js
hunterandroid10 小时前
Kotlin Coroutines 与 Flow:让异步任务更清晰
前端
Bigger10 小时前
从零搭建 AI 代码审查服务:一份前端也能看懂的 Python 学习笔记
前端·ci/cd·ai编程
lichenyang45311 小时前
JSAPI、NAPI、Biz、Imp:ASCF Demo 如何真正调用系统能力和 C++ 能力
前端
lichenyang45311 小时前
IPC、JSVM、UIThread、libuv:ASCF 架构图里最容易混的几个词
前端
用户0595401744611 小时前
Redis记忆存储故障恢复测试踩坑实录:手动测试让我漏掉了2个一致性Bug
前端·css
用户21366100357211 小时前
Vue2脚手架工程化与Axios集成
前端·vue.js
我不是外星人11 小时前
我把 Claude Code 搬到网页!自研高颜值 Web 交互工作台
前端·ai编程·claude