Vue3里ref最容易踩的坑和必学的核心技巧都有啥?
最近刷前端交流群,总能看到刚转Vue3的小伙伴问ref的问题:一会儿说ref的值用.value取不到了?一会儿又纠结什么时候用ref什么时候用reactive?还有人纳闷为啥循环里的ref模板引用不对?其实这些问题,只要摸透ref的底层逻辑和Vue3官方给的设计边界,都能迎刃而解,今天咱们就用问答的方式,从基础到进阶,把ref彻底讲透,连新手容易忽略的小细节都不放过。
先搞懂ref的底层:为什么它要有.value这个“麻烦”?
很多刚从Vue2转过来的同学,最不适应的就是ref后面跟着的.value——明明Vue2用this.$refs或者ref属性就能直接拿到DOM或组件实例,怎么到了Composition API里,连个简单的数字都得加个后缀?其实这不是Vue团队没事找事,反而恰恰是为了让数据响应式更可控、更符合JavaScript原生的行为逻辑。
你可以把ref想象成一个“透明的小盒子”:盒子的名字就是你定义的ref变量,盒子里面装的才是你真正要操作的值(可以是数字、字符串、布尔值、对象、数组,甚至是函数、DOM元素、组件实例),那为什么要用盒子呢?因为JavaScript里的基本类型(比如number、string、boolean、null、undefined、symbol)是“按值传递”的,而对象类型(比如object、array、function)是“按引用传递”的。
Vue3的响应式系统,底层用的是Proxy来代理对象类型,但Proxy没办法直接代理基本类型啊——比如你定义一个let count = 0,不管你怎么改count,它就是个值,不会触发任何代理逻辑,那怎么办?只能给基本类型套个“壳子”,也就是一个只有一个value属性的普通对象,然后用Proxy去代理这个壳子,这样的话,当你修改count.value的时候,Proxy就能监听到变化,然后去更新视图了。
那对象类型为啥有时候也用ref呢?这其实是个习惯问题,或者说场景问题,官方文档里也说了,ref可以接受任何类型的值——如果传的是对象或数组,Vue3内部会自动把这个对象/数组转换成reactive的响应式对象,然后把它赋值给壳子的value属性,也就是说,ref(obj)其实等价于reactive({ value: obj })吗?不对不对,等一下,我刚才是不是说错了?哦不,应该更严谨一点:ref处理非基本类型的时候,内部调用的是toReactive函数,把传入的原始值变成响应式对象,然后再把这个响应式对象赋值给.value,而如果你直接用reactive(obj),得到的就是一个本身就是响应式的对象,不需要再套壳子。
那这时候你可能会问了:既然ref能处理所有类型,那我以后都用ref不就行了?为啥还要留着reactive?别急,后面咱们讲ref和reactive的对比的时候会详细说,现在先记住这个“小盒子”的比喻,这对理解后面的所有内容都很重要。
Vue3 ref的基础用法:你真的会用吗?
基础用法1:定义响应式的基本类型数据
这应该是ref最常用的场景了,比如你要做一个计数器,代码可以这么写:
<script setup>
import { ref } from 'vue'
// 定义一个响应式的数字count,初始值是0
const count = ref(0)
// 定义一个响应式的字符串message,初始值是'Hello Vue3'
const message = ref('Hello Vue3')
// 定义一个响应式的布尔值isShow,初始值是true
const isShow = ref(true)
// 定义一个点击事件函数,修改count的值
const handleClick = () => {
// 注意哦,在<script setup>或者普通的Composition API函数里,修改ref的值必须加.value
count.value++
message.value = `你点击了${count.value}次`
if (count.value % 5 === 0) {
isShow.value = !isShow.value
}
}
</script>
<template>
<div>
<h1 v-if="isShow">{{ message }}</h1>
<button @click="handleClick">点我加1</button>
</div>
</template>
哎?这里有个奇怪的地方:在里用count、message、isShow的时候,为什么不用加.value?这是因为Vue3的模板编译器会自动帮你“解包”ref——只要你在里直接用的是ref的顶层变量(也就是没有放在对象或数组里的),编译器就会自动在后面加上.value,比如上面的{{ message }},编译器会把它转换成{{ message.value }},v-if="isShow"会转换成v-if="isShow.value",所以你在模板里不用操心这个。
基础用法2:定义响应式的对象/数组数据
刚才说了,ref可以接受任何类型的数据,包括对象和数组,比如你要做一个用户信息的表单,代码可以这么写:
<script setup>
import { ref } from 'vue'
// 定义一个响应式的对象user
const user = ref({
name: '张三',
age: 25,
hobbies: ['读书', '跑步', '看电影']
})
// 修改user的属性
const updateUser = () => {
// 注意哦,这里修改对象的属性,是不需要给user.value再套一层的,因为user.value本身已经是reactive的响应式对象了
user.value.name = '李四'
user.value.age++
// 修改数组也是一样的,直接用push、pop、shift、unshift这些原生方法就行,Vue3的Proxy能监听到
user.value.hobbies.push('打羽毛球')
}
// 那如果要替换整个user对象呢?
const replaceUser = () => {
// 这时候就必须给user赋值一个新的ref或者普通对象了吗?
// 不对,直接赋值给user.value就行,因为ref的壳子没变,只是里面的东西换了,这时候Proxy还是能监听到变化
user.value = {
name: '王五',
age: 30,
hobbies: ['游泳', '爬山']
}
}
</script>
<template>
<div>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<p>爱好:{{ user.hobbies.join(', ') }}</p>
<button @click="updateUser">更新用户信息</button>
<button @click="replaceUser">替换整个用户</button>
</div>
</template>
这里要注意两个点:第一,修改ref.value(对象/数组)的属性或元素时,不需要给user.value再套.value,因为它本身已经是响应式的了;第二,替换整个ref.value的时候,必须赋值给user.value,不能直接赋值给user,因为user是一个常量(用const定义的),不能重新赋值,而且就算你用let定义的,直接赋值给user也会丢失响应式,因为原来的“小盒子”被扔了,新的变量只是一个普通的值,没有被Proxy代理。
基础用法3:获取DOM元素或组件实例
这应该是Vue2开发者最熟悉的ref用法了,但在Vue3里,写法稍微有点不一样,而且功能也更强大了。
获取单个DOM元素
比如你要在页面加载完成后,自动聚焦到一个输入框,代码可以这么写:
<script setup>
import { ref, onMounted } from 'vue'
// 定义一个ref变量,初始值是null,用来接收DOM元素
const inputRef = ref(null)
// 页面挂载完成后执行
onMounted(() => {
// 这时候inputRef.value已经指向了输入框的DOM元素
inputRef.value.focus()
})
</script>
<template>
<div>
<!-- 给输入框绑定ref属性,值就是刚才定义的ref变量名 -->
<input type="text" ref="inputRef" placeholder="页面加载后自动聚焦">
</div>
</template>
这里要注意的是:第一,ref变量的初始值最好设为null,因为在页面挂载之前,DOM元素还不存在,inputRef.value就是null;第二,只有在onMounted钩子函数或者之后的生命周期钩子(比如onUpdated、onUnmounted等)里,才能拿到正确的DOM元素或组件实例,在setup函数的顶层是拿不到的,因为setup函数是在组件挂载之前执行的。
获取单个组件实例
比如你有一个子组件Child.vue,里面有一个方法叫showAlert,你想在父组件里调用这个方法,代码可以这么写:
先写Child.vue:
<script setup>
import { ref } from 'vue'
// 定义一个响应式的布尔值
const isAlertShow = ref(false)
// 定义一个方法
const showAlert = () => {
isAlertShow.value = true
setTimeout(() => {
isAlertShow.value = false
}, 2000)
}
// 注意哦!在Vue3的<script setup>语法糖里,组件内部的变量和方法默认是私有的,父组件拿不到
// 如果想让父组件拿到,必须用defineExpose暴露出来
defineExpose({
showAlert
})
</script>
<template>
<div>
<p>我是子组件</p>
<div v-if="isAlertShow" class="alert">我是子组件的弹窗,2秒后消失</div>
</div>
</template>
<style scoped>
.alert {
padding: 10px;
background-color: #f0f0f0;
border: 1px solid #ccc;
border-radius: 4px;
margin-top: 10px;
}
</style>
再写父组件Parent.vue:
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
// 定义一个ref变量,初始值是null,用来接收子组件实例
const childRef = ref(null)
// 页面挂载完成后执行
const handleCallChildMethod = () => {
// 调用子组件暴露出来的方法
childRef.value.showAlert()
}
</script>
<template>
<div>
<p>我是父组件</p>
<!-- 给子组件绑定ref属性,值就是刚才定义的ref变量名 -->
<Child ref="childRef" />
<button @click="handleCallChildMethod">调用子组件的方法</button>
</div>
</template>
这里有个超级重要的坑!很多刚转
code前端网