引言:新手的困惑


        很多 Vue 3 新手都会有一个疑问:为什么 ref 这个函数,既能创建响应式数据,又能获取 DOM 元素?这看起来违背了“单一职责原则”,让人有点糊涂。今天,我们就来深入源码设计理念,看看 Vue 团队为何做出这样的设计选择。

        如果你是新手可以从小节四看完再倒回来看比较好点!

一、回顾:ref 的双重身份

首先,我们明确一下 ref 在两种场景下的不同角色:

  1. 作为响应式 API(创建数据引用)

    import { ref } from 'vue';
    const count = ref(0); // 创建一个响应式的数字
    const message = ref('Hello'); // 创建一个响应式的字符串

    功能:将普通的 JavaScript 值(如数字、字符串)包裹成一个响应式对象。通过 .value 属性进行读写。

  2. 作为模板引用(获取 DOM 或组件实例)

    <template>
      <!-- 用在普通DOM标签上,获取的是DOM节点 -->
      <input ref="inputRef" type="text">
      <!-- 用在组件标签上,获取的是组件实例对象 -->
      <MyComponent ref="componentRef" />
    </template>
    
    <script setup>
    import { ref } from 'vue';
    // 声明一个同名的 ref 来接收引用
    const inputRef = ref(null); // 将来指向 <input> DOM 元素
    const componentRef = ref(null); // 将来指向 MyComponent 组件实例
    </script>

    功能:通过在与模板中 ref 属性同名的响应式引用里保存对应的实体。

二、从 Vue2 的直观到 Vue3 的统一

在 Vue2 中,ref 的使用简单明了,各司其职:

Vue2 的直观设计:
  • 模板中定义<div ref="myDiv"> - 这就是个标识符

  • JS 中获取this.$refs.myDiv - 直接拿到 DOM 节点

<template>
  <!-- 模板中定义:就是个标识符 -->
  <input ref="myInput" type="text">
  <div ref="myDiv">内容</div>
</template>

<script>
export default {
  methods: {
    focusInput() {
      // JS中获取:直接拿到DOM节点
      this.$refs.myInput.focus() // 简单直接!
      console.log(this.$refs.myDiv.innerHTML)
    }
  }
}
</script>

        Vue2 的设计很符合直觉:模板里标记,JS 里直接用this.$refs 就像个现成的工具箱,随手就能拿到需要的工具。

Vue3 的 ref:一个函数,双重身份

到了 Vue3,事情变得有些不同:

import { ref } from 'vue';

// 场景1:创建响应式数据
const count = ref(0);        // 响应式数字
const message = ref('Hello'); // 响应式字符串

// 场景2:承接模板引用
const inputRef = ref(null);   // 准备接收DOM元素
const componentRef = ref(null); // 准备接收组件实例

<template>
  <!-- 同样的 ref 属性,不同的引用目标 -->
  <input ref="inputRef" type="text">         <!-- 指向DOM元素 -->
  <MyComponent ref="componentRef" />         <!-- 指向组件实例 -->
</template>

        看到这里,很多新手都会困惑: 同一个 ref() 函数,怎么既管数据又管DOM?这不符合单一职责原则啊,还请往后续看

三 、深入剖析:为什么是“同一个” ref

        这并非是设计上的疏忽,而是一种深思熟虑的“约定优于配置”和“用户体验优先” 的体现。

核心理念:统一的“引用”概念在 Vue 的哲学中,ref 的核心语义是 “引用”(Reference)

当你写 const count = ref(0) 时,你是在创建一个对内部值的响应式引用。

当你写 <input ref="inputRef"> 和 const inputRef = ref(null) 时,你是在创建一个对 DOM 节点(或子组件)的模板引用。

            虽然它们引用的目标不同(一个是最小数据,一个是 DOM/组件),但它们在概念上都是一种 “引用”。Vue 选择用同一个 API(ref())来统一表达这种“引用”的概念。

    1. 降低心智负担与学习成本

    想象一下,如果 Vue 提供了两个不同的 API:

    createRef(data):用于创建响应式数据。
    
    templateRef(initialValue):用于创建模板引用。

            对于开发者而言,你需要记忆两个 API、它们的导入和用法。而实际上,它们在大多数使用场景下是泾渭分明的,很少会混淆。用一个 ref 统一管理所有“引用”,让 API 更简洁,减少了“选择困难”

    2. 技术实现的相似性
    从实现角度看,两种 ref 在底层有相似的结构:

    • 像前面的文章提出它们都是一个盒子对象,拥有一个 .value 属性。

    • 模板引用的 ref 在组件挂载前,其 .value 是 null;挂载后,.value 被设置为对应的 DOM 或组件实例。这个过程本身就是一个“响应式的变化”。当组件卸载时,它又会被设置回 null。你可以用 watchEffect 监听这个变化。

      //你可以把它看作是一个全自动监听响应式数据的变化的一个监听器
      watchEffect(() => {
        if (inputRef.value) {
          console.log('DOM元素已挂载:', inputRef.value);
          inputRef.value.focus(); // 自动聚焦
        } else {
          console.log('DOM元素已卸载');
        }
      });
      
      //watch对比watchEffect
      //watch:要明确指出监视的数据
      //watchEffect:不用明确指出监视的数据(函数中用到哪些属性,那就监视哪些属性)。

      你看,模板引用的生命周期天然就是响应式的,用 ref() 这个响应式 API 来承载它,在技术上是完美契合的。Vue3 的这种设计背后有着深刻的考量:

      3. 告别 this 的束缚

      Vue3 的 Composition API 强调函数式编程,不再依赖 this 上下文。在 setup 中根本没有 this,自然也就没有 this.$refs 了。

      4. 类型安全的胜利

              Vue2 的 this.$refs 在 TypeScript 中类型推导困难,而 Vue3 的变量声明方式让类型提示更加精准。

    5. 与 Composition API 的无缝集成
            Vue 3 的 Composition API 鼓励将逻辑组织在函数中,并自由地组合。无论是响应式数据还是模板引用,它们都是组合式函数可能操作的对象。使用统一的 ref 接口,使得任何操作 .value 的函数(无论是用于数据还是模板引用)在类型和用法上都是一致的,极大地提升了代码的灵活性和可组合性。

           讲了这么多我们来点实际的,新手接触肯定有困惑

    四、新手的困惑点

    困惑的不是概念,而是命名!

    问题在于:ref 这个名字被重载了:

    作为函数:ref() - 创建引用
    
    作为属性:ref="xxx" - 标记引用目标

    如果 Vue3 换个名字可能更清晰:

    // 假想的更清晰设计
    import { reactiveRef, templateRef } from 'vue'
    
    const data = reactiveRef(0)    // 明确是响应式数据
    const element = templateRef()  // 明确是模板引用

    实际使用中的心法

    记住这个口诀就行:

    • 模板里的 ref - 就是个"标记贴",贴在谁身上就引用谁

    • 代码里的 ref() - 就是个"遥控器",通过它来操作标记的目标

    <template>
      <!-- 贴标记:这个input叫"emailInput" -->
      <input ref="emailInput" type="email">
    </template>
    
    <script setup>
    // 拿遥控器:控制叫"emailInput"的那个东西
    const emailInput = ref(null)
    
    // 用遥控器操作
    const validateEmail = () => {
      if (emailInput.value) {
        console.log(emailInput.value.value) // 访问输入值
      }
    }
    </script>

    总结

    Vue3 的 ref 设计是为了:

    • 更好的 TypeScript 支持

    • 更一致的响应式体验

    • 摆脱 this 的依赖

    代价是:

    • 初学者需要适应期

    • 同一个名字承担了双重含义

            但一旦习惯后,你会发现这种统一性在复杂组件中反而更有优势。就像学开车,刚开始觉得手动挡麻烦,熟练后就享受那种精准控制的感觉了!

            Vue3 让 ref 身兼两职,可以理解为一种概念上的统一。它用初期的学习成本,换来了更好的类型支持、更灵活的代码组织和更一致的开发体验。就像从自动挡换到手动挡,开始可能觉得麻烦,但熟练后你会享受那种精准控制的快感。接下来,让我们深入看看这种设计背后的精妙之处...

    Logo

    Agent 垂直技术社区,欢迎活跃、内容共建。

    更多推荐