Vue 3 的 ref:为什么一个函数身兼两职?
引言:新手的困惑
很多 Vue 3 新手都会有一个疑问:为什么 ref 这个函数,既能创建响应式数据,又能获取 DOM 元素?这看起来违背了“单一职责原则”,让人有点糊涂。今天,我们就来深入源码设计理念,看看 Vue 团队为何做出这样的设计选择。
如果你是新手可以从小节四看完再倒回来看比较好点!
一、回顾:ref 的双重身份
首先,我们明确一下 ref 在两种场景下的不同角色:
-
作为响应式 API(创建数据引用)
import { ref } from 'vue'; const count = ref(0); // 创建一个响应式的数字 const message = ref('Hello'); // 创建一个响应式的字符串功能:将普通的 JavaScript 值(如数字、字符串)包裹成一个响应式对象。通过
.value属性进行读写。 -
作为模板引用(获取 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 身兼两职,可以理解为一种概念上的统一。它用初期的学习成本,换来了更好的类型支持、更灵活的代码组织和更一致的开发体验。就像从自动挡换到手动挡,开始可能觉得麻烦,但熟练后你会享受那种精准控制的快感。接下来,让我们深入看看这种设计背后的精妙之处...
更多推荐
所有评论(0)