当前位置:首页 > 前端 > 正文内容

深入 Vue 3 ref 的底层实现

virtualman2周前 (08-24)前端81

深入 Vue 3 ref 的底层实现:从响应式基石到自动解包之谜

在 Vue 3 的 Composition API 中,ref 是我们最常使用的响应式工具之一。它看似简单,但其底层实现却蕴含了 Vue 3 响应式系统的核心思想。本文将带你深入源码,剖析 ref 的创建、依赖收集、触发更新以及自动解包的完整机制。


一、ref 的核心作用

ref 用于将一个原始值(如 stringnumberboolean)或对象转换为响应式数据。与 reactive 不同,ref 返回的是一个包含 .value 属性的包装对象

import { ref } from 'vue'

const count = ref(0)
count.value++ // 修改值

二、ref 的底层实现原理

ref 的实现依赖于 Vue 3 的 reactive 系统和 effect 依赖追踪机制。其核心源码位于 packages/reactivity/src/ref.ts

1. 创建 refcreateRef

当我们调用 ref(value) 时,实际上是调用了 createRef 函数:

// 简化后的源码逻辑
function createRef(rawValue: unknown, shallow = false) {
  // 返回一个 RefImpl 实例
  return new RefImpl(rawValue, shallow)
}

RefImplref 的核心类:

class RefImpl<T> {
  private _value: T
  public readonly __v_isRef = true // 标记这是一个 ref

  constructor(private _rawValue: T, private _shallow = false) {
    // 如果是对象,使用 reactive 包装(深度 ref)
    this._value = _shallow ? _rawValue : reactive(_rawValue)
  }

  // getter: 读取 .value 时触发依赖收集
  get value() {
    // track 函数:收集当前活跃的 effect 作为依赖
    track(this, 'get', 'value')
    return this._value
  }

  // setter: 修改 .value 时触发依赖更新
  set value(newVal) {
    // 浅比较,避免不必要的更新
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      // 如果是对象,重新 reactive 包装
      this._value = this._shallow ? newVal : reactive(newVal)
      // trigger 函数:通知所有依赖更新
      trigger(this, 'set', 'value', newVal)
    }
  }
}

关键点解析:

  • __v_isRef 标志:用于在模板或 setup 中识别 ref,实现自动解包。
  • reactive 包装ref 内部对对象值使用 reactive 进行深层响应式处理(除非是 shallowRef)。
  • tracktrigger:这是响应式系统的“心脏”。track 在读取时收集依赖(如 render effect),trigger 在修改时通知依赖重新执行。
2. 依赖收集:track 函数

当组件渲染时访问 count.value,会触发 RefImplget value()。此时 track(this, 'get', 'value') 会被调用:

// track 函数简化逻辑
function track(target: RefImpl, type: TrackOpTypes, key: string) {
  // 获取当前正在执行的 effect(如组件的 render effect)
  const effect = activeEffect
  if (effect) {
    // 将 effect 添加到 target 的依赖集合中
    // 依赖存储在 WeakMap<target, Map<key, Set<effect>>> 中
    trackEffects(getDepFromTarget(target, key))
  }
}

这确保了当 ref 值变化时,所有依赖它的 effect(通常是组件渲染函数)都会被重新执行。

3. 触发更新:trigger 函数

count.value = 1 时,set value() 被调用,trigger(this, 'set', 'value', newVal) 执行:

function trigger(target: RefImpl, type: TriggerOpTypes, key: string, newValue: any) {
  // 从依赖映射中取出所有依赖此 key 的 effects
  const deps = getDepFromTarget(target, key)
  // 遍历并执行这些 effects
  triggerEffects(deps)
}

这会调度所有依赖该 refeffect 重新运行,从而更新视图。


三、ref 的自动解包(Auto-unwrapping)

这是 ref 最“神奇”的特性之一:在模板或 setup 返回的对象中,我们可以直接使用 count 而非 count.value

<template>
  <!-- 模板中直接使用 count,无需 .value -->
  <div>{{ count }}</div>
  <button @click="count++">+</button>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
// 在 script 中仍需 count.value
</script>

实现原理:

自动解包发生在 模板编译阶段响应式代理的 get 拦截器中。

  1. 模板编译:Vue 的模板编译器会静态分析模板,识别出变量名(如 count)并生成访问 .value 的代码:

    // 编译后类似
    () => _ctx.count.value
  2. 响应式代理拦截(关键):即使在 setup 返回的对象中,ref 也会被自动解包。这得益于 reactive 代理的 get 拦截器:

    // reactive 代理的 get 拦截器
    const get = (target, key, receiver) => {
      const res = Reflect.get(target, key, receiver)
      // 如果获取的值是 ref,且 key 不是 ref 的自有属性
      if (isRef(res) && !isObject(res)) {
        // 自动返回 .value(解包)
        return res.value
      }
      return res
    }

    因此,当你在模板或 setup 返回的对象中访问 state.count(假设 statereactive 对象,countref),代理会自动返回 count.value


四、shallowReftriggerRef

  • shallowRef:创建一个“浅层” ref,其 .value 不会被 reactive 包装。适用于大型对象或不可变数据,避免深度响应式带来的性能开销。
  • triggerRef:手动触发 shallowRef 的更新。因为 shallowRef.value 是普通对象,修改其内部属性不会自动触发 trigger,需手动调用 triggerRef(myShallowRef)

五、总结

ref 的底层实现精妙地结合了:

  1. 类封装RefImpl 管理值和响应式逻辑。
  2. 依赖追踪:通过 track/triggereffect 系统联动。
  3. 自动解包:编译时优化 + 运行时代理拦截,提升开发体验。

理解 ref 的实现,不仅有助于我们更好地使用 Vue 3,更能深入掌握其响应式设计的哲学:以最小的侵入性,实现最大的灵活性

小提示:在 script setup 中,ref.value 在模板中自动解包,但在 watchcomputed 等函数中仍需手动访问 .value。这是由其运行时机制决定的。

通过本文,希望你对 ref 不再是“黑盒”,而是能窥见其优雅的内部构造。

相关文章

【前端】pako库——数据压缩利器工具

【前端】pako库——数据压缩利器工具

pako.deflate()压缩,压缩为UInt8Array   pako.inflate()解压缩 let jsonObj = {"a":1,"b":"123"}; let jsonStr = JSON.stringify(jsonObj); let compresse...

CSS预处理器的优化与思考:从效率工具到工程化基石

一、引言:预处理器为何仍是前端工程的「刚需」?在原生CSS逐步支持变量(--var)、嵌套语法(CSS Nesting Level 3草案)的今天,有人质疑:“CSS预处理器是否即将退出历史舞台?” 但现实是,在中大型项目中,Sass、Less等工具依然是工程化的核心组件。它们解决的不仅是语法糖问题...

用Lottie做前端动画:从设计到落地的全流程实践

用Lottie做前端动画:从设计到落地的全流程实践

一、引言:为什么选择Lottie做动画?在前端开发中,实现复杂动画往往面临两大痛点: 手动编写CSS/JS动画代码繁琐:关键帧调试、性能优化耗时耗力,尤其是复杂交互动画 传统动画格式缺陷:GIF画质差、文件体积大;视频无法实现动态交互,且难以适配不同屏幕 Lottie的出现解决了这些...

【JS】什么是Document Fragment?

DocumentFragment 是 Web API 中的一个接口,表示一个没有父级的最小化文档对象。它被设计为一个轻量级的“文档片段”容器,可以用来存储一组节点,通常用于高效地进行 DOM 操作。 核心概念 虚拟容器:DocumentFragment 本身不是一个完整的文档,也不是实际 DOM...

【JS】Map和Object的区别

JavaScript 中的 Map 和 Object 都可以用来存储键值对,但它们在设计、性能和使用场景上有显著的区别。理解这些差异有助于在开发中做出更合适的选择。 1. 键的类型 Object: 键只能是字符串或Symbol。如果使用其他类型的值作为键(如数字、对象),它们会被自动转...

重排(Reflow)与重绘(Repaint)的区别?以及如何减少重排和重绘?

重排(Reflow)与重绘(Repaint):前端性能优化的基石在网页开发中,我们经常听到“重排”(Reflow)和“重绘”(Repaint)这两个术语。它们是浏览器渲染页面的核心过程,也是影响页面性能的关键因素。理解它们的区别和触发条件,是进行前端性能优化的基础。 一、浏览器渲染流程简述在深...

发表评论

访客

看不清,换一张

◎欢迎参与讨论,请在这里发表您的看法和观点。