目录
[一、ref 的核心原理](#一、ref 的核心原理)
[二、获取 DOM 元素](#二、获取 DOM 元素)
[常用 DOM 操作](#常用 DOM 操作)
[子组件:必须用 defineExpose 暴露](#子组件:必须用 defineExpose 暴露)
[父组件:通过 ref 调用](#父组件:通过 ref 调用)
[四、ref 的生命周期](#四、ref 的生命周期)
[五、v-for 中的 ref](#五、v-for 中的 ref)
[七、Vue 2 vs Vue 3 对比](#七、Vue 2 vs Vue 3 对比)
ref 是 Vue 提供的模板引用机制 ,通过在模板中给元素或组件添加 ref 属性,在 JS 中创建同名 ref 变量,Vue 会在组件挂载后自动将 DOM 元素或组件实例赋值给 ref.value,从而直接操作 DOM 或调用子组件方法。
一、ref 的核心原理
javascript
<template>
<!-- 2. 模板中写 ref="xxx" -->
<input ref="inputRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 1. JS 中创建同名 ref,初始值为 null
const inputRef = ref(null)
// 3. 组件挂载后,Vue 自动赋值:inputRef.value = DOM 元素
onMounted(() => {
inputRef.value.focus() // 可以直接操作 DOM
})
</script>
流程:
1. 创建 ref(null)
2. 模板渲染,发现 ref="inputRef"
3. 组件挂载完成
4. Vue 自动执行:inputRef.value = 这个 DOM 元素
5. 现在可以操作 DOM 了
二、获取 DOM 元素
基础用法
javascript
<template>
<div>
<input ref="inputRef" type="text" />
<button @click="focusInput">聚焦输入框</button>
<div ref="divRef" class="box">这是一个盒子</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const inputRef = ref(null)
const divRef = ref(null)
// 组件挂载后可以访问
onMounted(() => {
console.log(inputRef.value) // <input type="text">
console.log(divRef.value) // <div class="box">这是一个盒子</div>
console.log(divRef.value.offsetHeight) // 获取高度
})
const focusInput = () => {
inputRef.value?.focus() // 聚焦输入框
}
</script>
常用 DOM 操作
javascript
const divRef = ref(null)
// 获取元素尺寸
const getSize = () => {
const el = divRef.value
console.log('宽度:', el.offsetWidth)
console.log('高度:', el.offsetHeight)
console.log('位置:', el.getBoundingClientRect())
}
// 滚动到元素
const scrollToElement = () => {
divRef.value?.scrollIntoView({ behavior: 'smooth' })
}
// 操作样式
const changeStyle = () => {
divRef.value.style.backgroundColor = 'red'
}
三、获取组件实例(调用子组件方法)
子组件:必须用 defineExpose 暴露
javascript
<!-- Child.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 定义方法
const increment = () => {
count.value++
}
const reset = () => {
count.value = 0
}
const getCount = () => {
return count.value
}
// 👇 关键:暴露给父组件
defineExpose({
increment,
reset,
getCount,
count
})
</script>
<template>
<div>
<p>子组件计数:{{ count }}</p>
</div>
</template>
父组件:通过 ref 调用
javascript
<!-- Parent.vue -->
<template>
<div>
<Child ref="childRef" />
<button @click="handleIncrement">调用子组件 increment</button>
<button @click="handleReset">调用子组件 reset</button>
<button @click="handleGetCount">获取子组件 count</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
const handleIncrement = () => {
childRef.value?.increment() // 调用子组件方法
}
const handleReset = () => {
childRef.value?.reset()
}
const handleGetCount = () => {
alert(`当前计数:${childRef.value?.getCount()}`)
}
</script>
四、ref 的生命周期
| 阶段 | ref.value 的值 |
|---|---|
setup() 中 |
null(还没挂载) |
onBeforeMount |
null |
onMounted |
DOM 元素或组件实例 |
onBeforeUpdate |
更新前的值 |
onUpdated |
更新后的值 |
onBeforeUnmount |
还有值 |
onUnmounted |
null(组件已销毁) |
javascript
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'
const inputRef = ref(null)
console.log('setup:', inputRef.value) // null
onBeforeMount(() => {
console.log('onBeforeMount:', inputRef.value) // null
})
onMounted(() => {
console.log('onMounted:', inputRef.value) // <input> 元素
})
onBeforeUpdate(() => {
console.log('onBeforeUpdate:', inputRef.value) // 仍然有值
})
onBeforeUnmount(() => {
console.log('onBeforeUnmount:', inputRef.value) // 仍然有值
})
onUnmounted(() => {
console.log('onUnmounted:', inputRef.value) // null
})
</script>
五、v-for 中的 ref
javascript
<template>
<div>
<!-- v-for 中的 ref 需要用函数形式收集 -->
<div
v-for="item in list"
:key="item.id"
:ref="(el) => setItemRef(el, item.id)"
>
{{ item.name }}
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
{ id: 3, name: 'C' }
])
const itemRefs = ref([])
const setItemRef = (el, id) => {
if (el) {
itemRefs.value.push({ id, el })
}
}
// 或者用 Map 存储
const itemMap = ref(new Map())
const setItemMap = (el, id) => {
if (el) {
itemMap.value.set(id, el)
}
}
</script>
六、常用场景
场景1:输入框自动聚焦
javascript
<template>
<input ref="inputRef" v-if="showInput" />
<button @click="showInput = true">显示输入框</button>
</template>
<script setup>
import { ref, watch, nextTick } from 'vue'
const showInput = ref(false)
const inputRef = ref(null)
watch(showInput, async (newVal) => {
if (newVal) {
await nextTick() // 等待 DOM 更新
inputRef.value?.focus()
}
})
</script>
场景2:获取元素尺寸
javascript
<template>
<div ref="boxRef" class="box">内容</div>
<button @click="getSize">获取尺寸</button>
</template>
<script setup>
import { ref } from 'vue'
const boxRef = ref(null)
const getSize = () => {
if (boxRef.value) {
const { width, height, top, left } = boxRef.value.getBoundingClientRect()
console.log(`宽度:${width},高度:${height}`)
}
}
</script>
场景3:父组件重置子组件表单
javascript
<!-- 父组件 -->
<template>
<UserForm ref="formRef" />
<button @click="resetForm">重置表单</button>
</template>
<script setup>
import { ref } from 'vue'
import UserForm from './UserForm.vue'
const formRef = ref(null)
const resetForm = () => {
formRef.value?.reset()
}
</script>
javascript
<!-- UserForm.vue 子组件 -->
<script setup>
import { reactive } from 'vue'
const formData = reactive({
name: '',
email: ''
})
const reset = () => {
formData.name = ''
formData.email = ''
}
defineExpose({ reset })
</script>
七、Vue 2 vs Vue 3 对比
在 Vue 3 中,ref 是操作 DOM 元素和调用子组件方法的唯一官方推荐方式 。Vue 2 中的 $refs 仍然存在,但组合式 API 中统一使用 ref。
| 操作 | Vue 2(选项式 API) | Vue 3(组合式 API) |
|---|---|---|
| 获取 DOM | this.$refs.xxx |
const xxxRef = ref(null) |
| 调用子组件 | this.$refs.child.method() |
childRef.value.method() |
| 模板中写法 | ref="xxx" |
ref="xxxRef" |
八、面试回答
"
ref是 Vue 获取 DOM 元素或组件实例的方式。用法:
在 JS 中创建
const inputRef = ref(null)在模板中写
ref="inputRef"组件挂载后,
inputRef.value就是 DOM 元素或组件实例获取 DOM :直接操作元素,如
inputRef.value.focus()获取组件实例 :子组件需要用
defineExpose暴露方法,父组件通过childRef.value.method()调用生命周期 :
setup中是null,onMounted后才能拿到注意 :ref 名称必须匹配,调用时用可选链
?.防止报错,v-for 中需要用函数形式收集 ref 数组。"