讲解
Vue 的理念是「不直接碰 DOM」,但总有例外:聚焦输入框、初始化地图/图表库、测量元素尺寸、播放视频。这些场景用模板引用(template ref):模板里写 <input ref="inputEl">,脚本里声明同名 ref const inputEl = ref(null),挂载后 inputEl.value 就是真实 DOM 节点。注意时机:挂载前它是 null,所以访问要放在 onMounted 里。
ref 也能挂在子组件上:<Child ref="childRef"> 之后 childRef.value 拿到的是组件实例。但 <script setup> 的组件默认是封闭的——父组件拿不到它的内部状态,需要子组件用 defineExpose 显式暴露:defineExpose({ focus, reset })。这是个好设计:组件对外暴露的「命令式把手」被收敛成显式清单,而不是整个实例裸奔。
v-for 里的 ref 会得到数组:<li v-for="item in list" :ref="setItemRef"> 或 Vue 3.5 前的 ref="items" 自动收集(顺序不保证与数据源一致,别依赖它做索引映射)。函数式 ref :ref="(el) => ..." 更灵活,每次绑定更新都会调用。最后提醒:能用 props/emit 解决的交互就不要用 ref 命令式调用——ref 是逃生舱,不是正门。
示例
模拟 ref 从 null 到绑定的时机,以及 defineExpose 的「白名单」语义(在本教程构建时被真实执行):
import assert from 'node:assert/strict';
// 模板引用的生命周期:null → mounted 后可用
function createTemplateRef() {
let el = null;
return {
get value() {
return el;
},
_bind(domNode) {
el = domNode; // Vue 在挂载时帮你做的事
},
};
}
const inputEl = createTemplateRef();
assert.strictEqual(inputEl.value, null); // setup 阶段是 null
const fakeDom = { focus: () => '已聚焦', tagName: 'INPUT' };
inputEl._bind(fakeDom); // 挂载完成
assert.strictEqual(inputEl.value.tagName, 'INPUT');
assert.strictEqual(inputEl.value.focus(), '已聚焦');
// defineExpose:只暴露白名单里的成员
function createChildComponent() {
const internal = { secret: '内部状态,不外露' };
const exposed = {
reset: () => '已重置',
focus: () => '已聚焦',
};
return {
internal,
instance: new Proxy(exposed, {
get: (t, k) => (k in t ? t[k] : undefined), // 父组件只能摸到暴露的
}),
};
}
const child = createChildComponent();
assert.strictEqual(child.instance.reset(), '已重置');
assert.strictEqual(child.instance.secret, undefined); // 未暴露的拿不到
console.log('ref 在挂载前:', inputEl.value === null ? 'null' : '有值', '→ 挂载后可调用 focus()');
console.log('父组件只能访问 defineExpose 暴露的方法: reset / focus');
真实组件:
<script setup>
// SearchBox.vue
import { ref } from 'vue';
const inputEl = ref(null);
const keyword = ref('');
function focus() {
inputEl.value.focus();
}
function reset() {
keyword.value = '';
focus();
}
defineExpose({ focus, reset }); // 只暴露这两个方法
</script>
<template>
<input ref="inputEl" v-model="keyword" placeholder="搜索" />
</template>
<script setup>
// Parent.vue
import { ref, onMounted } from 'vue';
import SearchBox from './SearchBox.vue';
const box = ref(null);
onMounted(() => {
box.value.focus(); // 进入页面自动聚焦
});
</script>
<template>
<SearchBox ref="box" />
<button @click="box.reset()">清空并重置</button>
</template>
常见坑
- 挂载前访问 ref.value:顶层代码里它是 null,DOM 操作放 onMounted。
- 忘记声明同名 ref:模板 ref="xxx" 必须有对应的 const xxx = ref(null),Vue 3.5+ 可用 useTemplateRef。
- 依赖 v-for ref 数组的顺序:不保证和数据顺序一致,需要映射就用函数式 ref 自己建 Map。
- 把 ref 当常规通信手段:能声明式(props/emits)就别命令式(ref.xxx()),否则数据流会乱。
- 暴露整个实例:defineExpose 给最小集合,暴露越多,组件越难重构。
小结
ref="xxx" + 同名 ref,挂载后拿 DOM;组件实例默认封闭,defineExpose 开白名单;ref 是逃生舱不是正门。下一章把逻辑抽出组件:组合式函数。