前端-详解ref和$refs
目录
一. 什么是ref和$refs
二. 拿到的是什么?
三. 什么时候能用?
四. 是否响应式?
五. 典型用法(含踩坑点)
1.聚焦输入框(DOM 场景)
2.调子组件方法(实例场景)
3.v-for 场景(得到数组)
DOM 场景:多行列表的“可视化验证顺序”:
子组件场景:批量调用实例方法:
精准定位:用“唯一 ref 名称”避免顺序困扰
动态收集为“字典”:规模更大时更好管
和 v-if / v-show / 过渡 的差异
更新时机与“只读快照”的思维
一个“排序会坑你”的示例(生动版)
Vue3 顺带一嘴(你主要用 Vue2,可做迁移预备)
选型建议:
4.和 v-if 共用的坑
六. 设计建议(什么时候该用/不该用)
七. 和 $el 的区别
八. Vue 2 vs Vue 3(顺便扫盲)
九. 小型“速查清单”
一. 什么是ref和$refs
作用:利用ref和$refs可以用于获取dom元素,或组件实例
ref
是你在模板里贴的“便签”,$refs
是组件实例上收集所有便签的“通讯录”。
ref
(模板指令):写在标签或子组件上:<input ref="username">
、<Child ref="child" />
目标组件-添加ref属性
<BaseForm ref="baseForm"></BaseForm>
$refs
(实例属性):在 JS 里通过this.$refs.username
/this.$refs.child
取到刚才标记的东西
恰当时机,通过this.$refs.xxx,获取目标组件,就可以调用组件对象里面的方法
this.$refs.baseForm.组件方法()
👉 注意:Vue 里没有
refs
,只有$refs
。
二. 拿到的是什么?
ref
标在原生元素:拿到 DOM 节点
ref
标在子组件:拿到 子组件实例(可以调用它公开的方法)在
v-for
里多个同名ref
:this.$refs.xxx
会变成 数组(顺序=当次渲染顺序)
三. 什么时候能用?
created
阶段拿不到(DOM/子组件还没挂上)
mounted
及之后 才可用每次更新后(例如数据变了)要等 DOM 刷新:
this.$nextTick(() => { ... })
四. 是否响应式?
$refs
不是响应式的!
不能拿$refs.xxx
放到模板绑定里期待它自动更新;它只在渲染完成后被刷新一次快照。
五. 典型用法(含踩坑点)
1.聚焦输入框(DOM 场景)
<template><input ref="username" placeholder="用户名"> </template><script> export default {mounted() {// 挂载后再访问this.$refs.username.focus()} } </script>
2.调子组件方法(实例场景)
<!-- Parent.vue --> <template><ChildForm ref="form" /><button @click="submit">提交</button> </template><script> export default {methods: {submit() {// 直接调子组件公开方法(如 Element-UI 的 validate)this.$refs.form.validate(valid => {if (valid) { /* ... */ }})}} } </script>
3.
v-for
场景(得到数组)在
v-for
里给多个元素/子组件写同名ref
,渲染完成后this.$refs.xxx
会是 数组这个数组的顺序严格等于“当次渲染后的 DOM 顺序”(受你的
v-for
数据顺序、key
、过渡/重排影响)它不是响应式:每次更新刷新一遍快照,用的时候记得
this.$nextTick(...)
DOM 场景:多行列表的“可视化验证顺序”:
<template><ul><liv-for="todo in todos":key="todo.id"ref="rows">{{ todo.text }}</li></ul><button @click="prepend">在最前面插一条,观察 $refs 顺序变化</button> </template><script> export default {data() {return {todos: [{ id: 1, text: 'A' },{ id: 2, text: 'B' },{ id: 3, text: 'C' },]}},mounted() {// 初次渲染完成后,$refs.rows 是 [<li>A</li>, <li>B</li>, <li>C</li>]this.$nextTick(() => this.dumpRefs('mounted'));},methods: {prepend() {// 在最前面插入一条this.todos.unshift({ id: Date.now(), text: 'X' });this.$nextTick(() => this.dumpRefs('after prepend'));},dumpRefs(tag) {const txts = this.$refs.rows.map(li => li.textContent.trim());console.log(tag, txts);// 比如: mounted -> ["A", "B", "C"]// after prepend -> ["X", "A", "B", "C"]// 你会看到顺序完全等于当前 DOM 的顺序}} } </script>
要点:别用数组索引当“业务 ID”。一旦你在前面插入/排序,
$refs.rows[i]
对应的 DOM 就换人了。子组件场景:批量调用实例方法:
假设有一个子组件
EditableRow
,暴露了validate()
方法(Element‑UI/自研都类似):<!-- Parent.vue --> <template><EditableRowv-for="row in rows":key="row.id":model="row"ref="editors"/><el-button type="primary" @click="validateAll">全部校验</el-button> </template><script> export default {data() {return { rows: [{id:1},{id:2},{id:3}] }},methods: {validateAll() {// this.$refs.editors 是 [EditableRow实例, EditableRow实例, ...]this.$nextTick(() => {const list = this.$refs.editors || [];Promise.all(list.map(c => c.validate())).then(() => this.$message.success('全部通过')).catch(() => this.$message.error('有不通过的项'));})}} } </script>
精准定位:用“唯一 ref 名称”避免顺序困扰
当你必须通过
$refs
找到“某一条”时,不要依赖数组索引,给每一项做独一无二的 ref 名:<liv-for="todo in todos":key="todo.id":ref="'row-' + todo.id" >{{ todo.text }} </li>
使用:
this.$nextTick(() => {const el = this.$refs['row-' + targetId]; // 单个 DOM,而不是数组el && el.scrollIntoView({ block: 'center' }); });
优点:无论插入/删除/重排多少次,都能稳准狠找到对应元素
动态收集为“字典”:规模更大时更好管
如果你想同时拿到字典形式(
id -> 元素/实例
),可用函数式 ref(Vue2 里也能这么写):<liv-for="todo in todos":key="todo.id":ref="el => setRef(el, todo.id)" >{{ todo.text }} </li>
export default {data() {return { itemRefs: {} } // { [id]: HTMLElement }},methods: {setRef(el, id) {// el 为 null 时表示该 DOM 被卸载,清理一下if (el) this.$set(this.itemRefs, id, el);else this.$delete(this.itemRefs, id);},focusById(id) {this.$nextTick(() => this.itemRefs[id]?.focus?.());}} }
优点:拿到的是稳定的“映射表”,跟着你的业务主键走,不被渲染顺序牵着鼻子走。
和
v-if / v-show / 过渡
的差异
v-if="false"
:该项不渲染 → 不会出现在$refs.xxx
数组里(或被移除)
v-show="false"
:DOM 仍在,只是display:none
→ 仍然在$refs.xxx
数组中
<transition-group>
或排序动画:最终数组顺序 = 动画结束后的 DOM 顺序;渲染期间数组可能临时处于中间态,读值务必放nextTick
或动画钩子后。更新时机与“只读快照”的思维
在
created
钩子里还没有 DOM →$refs
为空在
mounted/updated
之后访问,或改完数据后this.$nextTick(...)
$refs
不是响应式:不要用它来驱动模板;只作为命令式把手(focus、scroll、measure、调用方法)一个“排序会坑你”的示例(生动版)
<template><ul><li v-for="user in usersSorted" :key="user.id" ref="items">{{ user.score }} - {{ user.name }}</li></ul><button @click="sortDesc">按分数降序</button><button @click="addOne">给张三 +10 分</button> </template><script> export default {data() {return {users: [{ id: 1, name: '张三', score: 80 },{ id: 2, name: '李四', score: 90 },{ id: 3, name: '王五', score: 85 }],desc: false}},computed: {usersSorted() {const arr = this.users.slice().sort((a, b) =>this.desc ? b.score - a.score : a.score - b.score);return arr;}},methods: {sortDesc() {this.desc = true;this.$nextTick(() => console.log(this.$refs.items.map(li => li.textContent.trim())));// 输出顺序会立刻变为降序},addOne() {const zhang = this.users.find(u => u.id === 1);zhang.score += 10;this.$nextTick(() => {// 再次输出,你会看到张三可能“跳了位置”console.log(this.$refs.items.map(li => li.textContent.trim()))});}} } </script>
结论:
$refs.items[i]
代表“第 i 个 DOM”,不是“ID=某人”的 DOM。别把索引当 ID。Vue3 顺带一嘴(你主要用 Vue2,可做迁移预备)
Vue3(Composition API)里如果写
<div ref="els">
且脚本const els = ref(null)
,不会自动变成数组,只会拿到最后一个。推荐用函数式 ref + 手动数组/Map收集,并在
onBeforeUpdate
里清空一次,避免旧引用残留:<script setup> import { ref, onBeforeUpdate } from 'vue' const itemEls = ref([]) // 或者用 Map:ref(new Map()) onBeforeUpdate(() => { itemEls.value = [] }) const setItemEl = el => { if (el) itemEls.value.push(el) } </script><template><li v-for="todo in todos" :key="todo.id" :ref="setItemEl">{{ todo.text }}</li> </template>
选型建议:
需要“对每个项做一次命令式操作”(滚动、测量、调用子组件方法):用同名
ref
(数组)+nextTick
。需要“按业务主键精确定位某项”:别用数组;用唯一 ref 名或函数式 ref 收集成字典。
4.和
v-if
共用的坑v-if为false时,该ref不存在(undefined或从$refs中移除)
解决:访问前先判断空,或把逻辑放nextTick里:
this.$nextTick(() => {const dlg = this.$refs.dialogif (dlg) dlg.open() })
5.和
<transition-group>
/排序的坑列表有过渡或重排,
$refs.xxx
的数组顺序跟随“当次渲染顺序”,不要把它当成稳定索引映射;真正的业务映射请用你的数据源(id
/map
),$refs
仅作“操作 DOM/实例”的临时入口。
六. 设计建议(什么时候该用/不该用)
✅ 该用
ref/$refs
的场景
操作真实 DOM:聚焦、滚动、测量尺寸、操控 Canvas、第三方库挂载点
调用子组件公开方法:如
validate() / resetFields() / open()
等❌ 不该用的场景
数据流转:父子通信请用
props
+emit
(或.sync
),不要通过$refs
去改子组件内部数据状态展示:
$refs
不是响应式,不要拿它去“驱动视图”心法:能声明式就声明式(
props/emit/v-model
),只有当“必须命令式”时再上$refs
。
七. 和 $el
的区别
this.$el
:当前组件根 DOM
this.$refs.xxx
:子元素/子组件的句柄(可能是很多个)
八. Vue 2 vs Vue 3(顺便扫盲)
Vue 2(你当前项目场景):用字符串
ref
,在 JS 里this.$refs.xxx
Vue 3 Composition API:
模板里
<div ref="el">
搭配脚本里const el = ref(null)
(同名变量自动注入)仍有
$refs
(在 Options API 中),但更推荐上面这种“变量式模板 ref”注意区分
ref()
(响应式引用) 和 模板ref
属性:同名不同物。
九. 小型“速查清单”
ref
标元素 → DOM;标组件 → 组件实例
v-for
同名ref
→ 数组
$refs
只在mounted/updated
后可用,更新读取请配合this.$nextTick
$refs
非响应式,只做“临时把手”,别驱动 UI与
v-if
搭配注意判空;与排序/过渡搭配不要依赖顺序稳定